Move towards a Single Page Application (SPA) approach

This commit is contained in:
0x1eef 2024-10-17 21:29:07 -03:00
parent 5d4d5a5e8d
commit eec625498d
51 changed files with 3354 additions and 761 deletions

2
.bundle/config Normal file
View file

@ -0,0 +1,2 @@
---
BUNDLE_PATH: ".gems"

View file

@ -5,7 +5,6 @@ source "https://rubygems.org"
# nanoc
gem "nanoc", "~> 4.12"
gem "nanoc-webpack.rb", "~> 0.10"
gem "nanoc-tidy.rb", "~> 0.8.4"
##
# dev

View file

@ -60,9 +60,6 @@ GEM
nanoc-checking (~> 1.0)
nanoc-cli (~> 4.11, >= 4.11.15)
nanoc-core (~> 4.11, >= 4.11.15)
nanoc-tidy.rb (0.8.4)
ryo.rb (~> 0.5)
test-cmd.rb (~> 0.12.4)
nanoc-webpack.rb (0.10.6)
ryo.rb (~> 0.5)
test-cmd.rb (~> 0.12.4)
@ -149,7 +146,6 @@ PLATFORMS
DEPENDENCIES
listen (~> 3.0)
nanoc (~> 4.12)
nanoc-tidy.rb (~> 0.8.4)
nanoc-webpack.rb (~> 0.10)
paint (~> 2.3)
rake (~> 13.2)

37
Rules
View file

@ -7,7 +7,6 @@ require "ryo"
require "ryo/json"
require "ryo/yaml"
require "nanoc-webpack"
require "nanoc-tidy"
##
# Extensions
@ -23,36 +22,32 @@ tdata = Ryo.from_json(path: File.join(dirs.content, "json", "t.json"))
surahs = Ryo.from_json(path: File.join(dirs.content, "json", "surahs.json"))
tidy = `which tidy || which tidy5`.chomp
buildenv = ENV["buildenv"] || "development"
etcdir = File.join(__dir__, "etc")
globals = {buildenv:, locales:, tidy:, tdata:, surahs:, name_by_id:}
##
# Filters
Nanoc::Tidy
.default_argv
.replace([*Nanoc::Tidy.default_argv, "-upper"].uniq)
##
# Rules
passthrough "/json/durations/*.json"
passthrough "/json/**/*.json"
require_rules "nanoc/rules/assets"
require_rules "nanoc/rules/redirect", globals
require_rules "nanoc/rules/random", globals
require_rules "nanoc/rules/surah-stream", globals
require_rules "nanoc/rules/surah-index", globals
compile "/js/main/vendor.ts" do
compile "/js/vendor.ts" do
filter :webpack,
argv: %w[--config etc/webpack.vendor.js]
write("/js/main/vendor.js")
write("/js/vendor.js")
end
compile("/js/index.tsx") do
filter :webpack,
argv: ["--config", "etc/webpack.#{buildenv}.js"],
depend_on: ["/js/components", "/js/lib", "/js/hooks", "/css"]
write("/js/index.js")
end
compile("/html/index.html") do
write("/index.html")
end
compile("/manifest.webapp") do
write("/manifest.webapp")
end
compile("/**/*") { write(nil) }
layout("**/*", :erb)
postprocess do
# Remove build artifacts
system "rm -rf #{nanoc.output_dir}/json/"
end

19
package-lock.json generated
View file

@ -11,7 +11,8 @@
"dependencies": {
"@preact/compat": "^17.1.2",
"classnames": "^2.3",
"preact": "^10.23.2"
"preact": "^10.23.2",
"preact-router": "^4.1.2"
},
"devDependencies": {
"@babel/preset-env": "^7.25.4",
@ -3059,9 +3060,9 @@
}
},
"node_modules/babel-loader": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz",
"integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==",
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz",
"integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -5227,6 +5228,15 @@
"url": "https://opencollective.com/preact"
}
},
"node_modules/preact-router": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/preact-router/-/preact-router-4.1.2.tgz",
"integrity": "sha512-uICUaUFYh+XQ+6vZtQn1q+X6rSqwq+zorWOCLWPF5FAsQh3EJ+RsDQ9Ee+fjk545YWQHfUxhrBAaemfxEnMOUg==",
"license": "MIT",
"peerDependencies": {
"preact": ">=10"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -6381,6 +6391,7 @@
"license": "0BSDL",
"devDependencies": {
"@types/node": "^22.0",
"babel-loader": "^9.2.1",
"typescript": "^5.5"
}
}

View file

@ -9,9 +9,10 @@
"eslint:apply": "npx eslint --config etc/eslint.config.mjs --fix src/js/"
},
"dependencies": {
"preact": "^10.23.2",
"@preact/compat": "^17.1.2",
"classnames": "^2.3"
"classnames": "^2.3",
"preact": "^10.23.2",
"preact-router": "^4.1.2"
},
"devDependencies": {
"@babel/preset-env": "^7.25.4",

View file

@ -0,0 +1,17 @@
root = true
[*.html]
indent_style = space
indent_size = 2
[*.rb, *.erb]
indent_style = space
indent_size = 2
[*.js, *.ts, *.tsx]
indent_style = space
indent_size = 2
[*.scss]
indent_style = space
indent_size = 2

View file

View file

@ -1,11 +1,13 @@
{
"name": "Quran",
"version": "0.1.0",
"description": "The Noble Quran: a programmer's interface",
"description": "A programmer's interface to The Noble Quran",
"main": "dist/index.js",
"types": ["dist/index.d.ts"],
"types": [
"dist/index.d.ts"
],
"scripts": {
"build": "npm exec tsc",
"build": "npx webpack --config etc/webpack.config.js",
"prepare": "npm run build"
},
"repository": {
@ -16,6 +18,7 @@
"license": "0BSDL",
"devDependencies": {
"@types/node": "^22.0",
"babel-loader": "^9.2.1",
"typescript": "^5.5"
}
}

View file

@ -36,14 +36,26 @@ class Quran {
readonly surahs: Surah[];
/**
* @returns {Array} The available locales
* @returns {Record<string, TLocale>} The available locales
*/
static get locales(): TLocale[] {
return [
{"name": "en", "displayName": "English", "direction": "ltr"},
{"name": "ar", "displayName": "العربية", "direction": "rtl"},
{"name": "fa", "displayName": "فارسی", "direction": "rtl"}
];
static get locales(): Record<string, TLocale> {
return {
"en": {"name": "en", "displayName": "English", "direction": "ltr"},
"ar": {"name": "ar", "displayName": "العربية", "direction": "rtl"},
"fa": {"name": "fa", "displayName": "فارسی", "direction": "rtl"}
}
}
/**
* @returns {Record<string, Surah[]>} The available surahs
*/
static get surahs(): Record<string, Surah[]> {
const result: Record<string, Surah[]> = {}
const surahs: Record<string, TSurah[]> = require("@json/surahs");
for (const locale in surahs) {
result[locale] = surahs[locale].map((surah: TSurah) => new Surah(surah));
}
return result;
}
constructor(self: TQuran) {

File diff suppressed because it is too large Load diff

View file

@ -2,56 +2,28 @@
** vNEXT
**** Remove ~kanit-regular.ttf~
This change falls back onto standard web fonts across all
languages. This should help decrease bundle size, and save
time processing the custom font during page load
** Add ~Quran.surahs~
The ~Quran.surahs~ getter returns an object where the
key is the locale name, and the value is an array of
Surah objects
**** Add src/js/main/vendor.ts
Add a new entry point that bundles preact, and other third
party dependencies. Third-party imports are now in a single
place rather than duplicated throughout app components
**** Reduce bundle size
Remove build artifacts and features that don't apply on
KaiOS (eg: opengraph tags)
**** Replace the play|pause icons
** Replace the play|pause icons
Replace the play and pause icons
**** Remove nanoc-gzip.rb
** Remove nanoc-gzip.rb
Unneccessary for a packaged KaiOS application
**** Remove loaders
** Remove loaders
Remove all loaders from ~/src/js/loaders/~, and the postman
package (~packages/typescript/postman~)
**** KaiOS fork
Fork a new branch: ~kaios/main~ dedicated to KaiOS
** Replace multiple entry points with a single entry point
The ~/src/js/main/*~ directory - which previously contained
multiple entry points - has been reduced to a single entry
point: ~/src/js/index.tsx~
**** Add ~etc/~
Move a large portion of the website's configuration files to
the ~/etc~ directory
** v0.9.0
**** Add ~share/al-quran.reflectslight.io/documentation/~
Replace ~share/doc/al-quran.reflectslight.io~
**** Add new recitation
Add a new recitation by Hani ar-Rifai
**** Replace ~opengraph.rb~ with ~_opengraph.html.erb~
Simplify how we render opengraph meta tags
**** Move to nodejs for scss compiler
Replace the deprecated Ruby scss compiler with the nodejs compiler
**** Add ~audio.base_url~ to nanoc.yaml
Provide extra flexibility for audio content
**** Rename packages/typescript/Quran/ properties
Introduce urlName, translitName to Surah objects
**** eslint upgrade
Migrate to the most recent version of eslint (^9.8)
** Redesign as a Single Page Application (SPA)
The previous approach where we generated a HTML file
for every surah in each language has been replaced by
a Single Page Application (SPA) that is more suitable
for KaiOS.

View file

@ -1,4 +1,3 @@
@import "base/colors";
@import "base/breakpoints";
@import "base/icon";
@import "base/select";
@ -8,46 +7,111 @@ html {
height: 100%;
body {
height: 100%;
color: $black;
margin: 0;
.root {
height: 100%;
}
.ltr {
font-family: 'Arial', 'Tahoma', sans-serif;
font-family: "Kanit Regular";
direction: ltr;
}
.rtl {
font-family: 'Arial', 'Tahoma', sans-serif;
font-family: "Cairo Regular", "Arial", "Tahoma", sans-serif;
direction: rtl;
}
.invisible, .hidden {
display: none;
}
.outline-0 {
outline: 0;
}
.scroll-y {
overflow-y: auto;
}
.font-cairo {
font-family: "Cairo Regular";
}
.font-cairo-bold {
font-family: "Cairo Bold";
}
.font-amiri {
font-family: "Amiri Regular", "Scheherazade", "Arial", sans-serif;
}
}
}
body .root .content.theme {
margin: 0 auto;
max-width: $breakpoint-md;
max-width: $breakpoint-sm;
width: 100%;
color: var(--color-accent);
/* <= $breakpoint-sm */
@media (max-width: $breakpoint-sm) {
width: 85%;
font-size: medium;
}
header {
a[data-testid="h1"] {
font-size: large;
background: var(--primary-color);
color: var(--secondary-color);
}
}
ul.body {
scrollbar-color: var(--primary-color) var(--secondary-color);
}
.color-primary {
color: var(--primary-color);
}
.color-secondary {
color: var(--secondary-color) !important;
}
.color-accent {
color: var(--accent-color);
}
.background-primary {
background: var(--primary-color);
}
.background-secondary {
background: var(--secondary-color);
}
.background-accent {
background: var(--accent-color);
}
.border-accent {
border: 1px solid var(--accent-color);
}
}
/**
* RTL languages
*/
body .root .content.theme.rtl {
direction: rtl;
header a[data-testid="h1"] {
font-family: "Cairo Bold";
}
}
html[dir="rtl"] {
.font-extrabold {
font-family: "Cairo Bold" !important;
}
}

View file

@ -1,8 +1,4 @@
/* KaiOS: max widths */
$breakpoint-kaiOS-portrait: 240px;
$breakpoint-kaiOS-landscape: 320px;
/* Standard max widths */
/* max-width */
$breakpoint-sm: 576px;
$breakpoint-md: 768px;
$breakpoint-lg: 992px;

View file

@ -1,2 +0,0 @@
$black: #454545;
$white: #FFF;

View file

@ -57,6 +57,7 @@
.refresh.icon,
.right-arrow.icon,
.left-arrow.icon {
fill: var(--primary-color);
height: 16px;
width: 16px;
}
@ -65,6 +66,14 @@
transform: rotate(180deg);
}
.pause.icon {
g rect {
width: 15px;
height: 40px;
fill: var(--primary-color);
}
}
ul.body.stream span {
.sound-on.icon,
.sound-off.icon {
@ -92,7 +101,7 @@
.root .content.theme.rtl {
.stalled.icon {
left: 0px;
right: 7px;
right: 12px;
}
ul.body.stream {
@ -112,3 +121,42 @@
}
}
}
.content.theme {
ul.body.stream li, footer {
.play.icon {
fill: var(--primary-color);
stroke: var(--secondary-color);
stroke-width: 2px;
}
.pause.icon {
rect {
fill: var(--primary-color);
stroke: var(--secondary-color);
stroke-width: 1px;
}
}
.refresh.icon {
fill: var(--primary-color);
}
.sound-on.icon, .sound-off.icon {
polygon {
fill: var(--primary-color);
stroke-width: 2;
}
}
.right-arrow.icon,
.left-arrow.icon {
g {
fill: var(--secondary--color);
}
}
}
.stalled.icon {
div { background: var(--secondary-color); }
}
}

View file

@ -1,22 +1,37 @@
.root .content.theme {
.green {
background: #6d765b !important;
}
.blue {
background: #3383C3 !important;
}
.react-select {
z-index: 2;
.active {
cursor: pointer;
}
}
.react-select.theme-select {
ul li,
ul li a {
-webkit-tap-highlight-color: transparent;
}
.circle {
width: 16px;
height: 16px;
border-radius: 12px;
background: var(--primary-color);
}
}
.react-select.language-select {
li a {
border: 1px solid $black;
background: var(--primary-color);
color: var(--secondary-color);
&:active, &:visited, &:link {
color: var(--secondary-color);
}
&:hover {
font-weight: bold;
}
}
}
}

View file

@ -4,8 +4,9 @@
body .root .content.theme {
ul.body.index {
@media (max-width: $breakpoint-sm) {
li, li a {
color: var(--accent-color);
@media (max-width: $breakpoint-sm) {
width: 100%;
}
}
@ -30,9 +31,48 @@ body .root .content.theme {
}
footer {
/* TODO: Remove footer ? */
a {
color: var(--accent-color);
&:active, &:link, &:visited {
color: var(--accent-color);
}
&:hover {
font-weight: bold;
}
}
input {
color: var(--accent-color);
border: 1px solid var(--primary-color);
&:focus {
outline-color: var(--primary-color);
}
}
@media(max-width: $breakpoint-sm) {
border-top: 1px solid #f2f2f2;
.right-arrow,
.left-arrow {
display: none;
}
a {
width: 100%;
justify-content: center;
}
}
@media(max-width: $breakpoint-sm) {
input[data-testid="SurahIndex/Filter"] {
display: none;
}
}
@media(hover: none) {
input[data-testid="SurahIndex/Filter"] {
display: none;
}
}
}
}
/**
@ -40,24 +80,19 @@ body .root .content.theme {
*/
body .root .content.theme.rtl {
ul.body.index {
li a {
span:first-child {
border: 1px solid $black;
margin-left: 0.5rem;
}
}
li.surah {
@media (max-width: $breakpoint-sm) {
width: 100%;
}
span.transliterated { display: none; }
}
}
/* >= $breakpoint-xxl */
@media (min-width: $breakpoint-xxl) {
ul.body.index {
li.surah a {
span:last-child {
font-size: larger;
@media (hover: hover) {
footer {
a {
&:hover {
font-weight: normal;
font-family: "Cairo Bold";
}
}
}

View file

@ -16,40 +16,13 @@ body .root .content.theme {
animation: FadeIn 1s;
}
}
ul.body.stream:focus {
outline: none;
}
}
body .root .content.theme.rtl {
ul.body.stream {
li.ayah p {
line-height: 1.7;
max-width: 470px;
}
}
/* <= $breakpoint-sm */
@media (max-width: $breakpoint-sm) {
ul.body.stream {
li.ayah p {
width: 100%;
}
}
}
/* >= $breakpoint-xxl */
@media (min-width: $breakpoint-xxl) {
ul.body.stream {
$gap: 2rem;
margin-top: $gap;
li.ayah {
font-size: larger;
margin-bottom: $gap;
}
}
footer {
.timer {
font-size: larger;
}
@extend .font-amiri;
}
}
}

View file

@ -1,39 +1,5 @@
.root .content.theme.blue.rtl {
direction: rtl;
}
.root .content.theme.blue {
@import "blue/base/colors";
.color-white {
color: $white;
--primary-color: #3383C3;
--secondary-color: #FFF;
--accent-color: #444;
}
.color-primary {
color: $primary-color;
}
.color-secondary {
color: $secondary-color;
}
.color-accent {
color: $accent-color;
}
.background-primary {
background: $primary-color;
}
.background-secondary {
background: $secondary-color;
}
.background-accent {
background: $accent-color;
}
}
@import "blue/base";
@import "blue/main/SurahIndex";
@import "blue/main/SurahStream";

View file

@ -1,15 +0,0 @@
@import "base/icon";
@import "base/select";
.root .content.theme.blue {
@import "base/colors";
scrollbar-color: $primary-color #FFF;
header {
a[data-testid="h1"] {
background: $primary-color;
border: 1px solid $black;
color: #FFF;
}
}
}

View file

@ -1,4 +0,0 @@
$primary-color: #3383C3;
$secondary-color: #3383C3;
$accent-color: lighten($primary-color, 38%);
$white: #FFF;

View file

@ -1,40 +0,0 @@
.content.theme.blue {
@import "themes/blue/base/colors";
ul.body.stream li, footer {
.play.icon {
fill: $primary-color;
stroke: $secondary-color;
stroke-width: 2px;
}
.pause.icon {
path {
fill: $primary-color;
stroke: $secondary-color;
stroke-width: 1px;
}
}
.refresh.icon {
fill: $primary-color;
}
.sound-on.icon, .sound-off.icon {
polygon {
fill: $primary-color;
stroke-width: 2;
}
}
.right-arrow.icon,
.left-arrow.icon {
g {
fill: $secondary-color;
}
}
}
.stalled.icon {
div { background: $secondary-color; }
}
}

View file

@ -1,29 +0,0 @@
.content.theme {
@import "themes/blue/base/colors";
.blue {
background: $secondary-color;
}
}
.content.theme.blue {
@import "themes/blue/base/colors";
.react-select.theme-select {
.active .circle {
background: $secondary-color;
}
ul li.blue .circle {
background: $secondary-color;
}
}
.react-select.language-select {
li a {
background: $secondary-color;
color: $white;
&:active, &:visited, &:link {
color: $white;
}
}
}
}

View file

@ -1,58 +0,0 @@
.root .surah-index.content.theme.blue {
@import "themes/blue/base/colors";
@import "base/breakpoints";
ul.body.index {
li.surah a {
&:active, &:link, &:visited {
color: $primary-color;
}
span:first-child {
border: 1px solid $black;
color: $white;
}
}
}
footer {
a {
&:active, &:link, &:visited {
color: $primary-color;
text-decoration: none;
}
&:hover {
font-weight: bold;
}
}
input {
border: 1px solid $secondary-color;
&:focus {
outline-color: $secondary-color;
}
}
}
}
.root .content.theme.blue.en {
@import "themes/blue/base/colors";
header {
div {
color: $secondary-color;
}
}
}
.root .content.theme.blue.rtl {
@import "themes/blue/base/colors";
ul.body.index {
li.surah a {
span.id {
color: $secondary-color;
}
span.name {
color: $primary-color;
}
}
}
}

View file

@ -1,19 +0,0 @@
.root .content.theme.blue {
@import "themes/blue/base/colors";
@import "base/breakpoints";
ul.body.stream {
li.ayah {
span span {
color: $secondary-color;
}
p { }
}
}
footer {
.timer {
color: $primary-color;
}
}
}

View file

@ -1,40 +1,5 @@
.root .content.theme.green.rtl {
@import "green/base/colors";
direction: rtl;
}
.root .content.theme.green {
@import "green/base/colors";
.color-primary {
color: $primary-color;
--primary-color: #6d765b;
--secondary-color: #FFF;
--accent-color: #444;
}
.color-secondary {
color: $secondary-color;
}
.color-accent {
color: $accent-color;
}
.color-white {
color: #FFF;
}
.background-primary {
background: $primary-color;
}
.background-secondary {
background: $secondary-color;
}
.background-accent {
background: $accent-color;
}
}
@import "green/base";
@import "green/main/SurahIndex";
@import "green/main/SurahStream";

View file

@ -1,15 +0,0 @@
@import "base/icon";
@import "base/select";
.root .content.theme.green {
@import "base/colors";
scrollbar-color: $primary-color #FFF;
header {
a[data-testid="h1"] {
background: $primary-color;
border: 1px solid $black;
color: #FFF;
}
}
}

View file

@ -1,3 +0,0 @@
$primary-color: #6d765b;
$secondary-color: #6C755A;
$accent-color: #FFF;

View file

@ -1,41 +0,0 @@
.content.theme.green {
@import "themes/green/base/colors";
ul.body.stream li, footer {
.play.icon {
g path {
fill: $primary-color;
stroke: $primary-color;
stroke-width: 3px;
}
}
.pause.icon {
g path {
fill: $primary-color;
stroke: $primary-color;
stroke-width: 1px;
}
}
.refresh.icon {
fill: $primary-color;
}
.sound-on.icon, .sound-off.icon {
g polygon {
fill: $primary-color;
}
}
.right-arrow.icon,
.left-arrow.icon {
g {
fill: $primary-color;
}
}
}
.stalled.icon {
div { background: $primary-color; }
}
}

View file

@ -1,29 +0,0 @@
.content.theme {
@import "themes/green/base/colors";
.green {
background: $primary-color;
}
}
.content.theme.green {
@import "themes/green/base/colors";
.react-select.theme-select {
.active {
.circle {
background: $primary-color;
border-radius: 10px;
}
}
}
.react-select.language-select {
li a {
background: $primary-color;
color: $white;
&:active, &:visited, &:link {
color: $white;
}
}
}
}

View file

@ -1,31 +0,0 @@
.root .surah-index.content.theme.green {
@import "base/breakpoints";
@import "themes/green/base/colors";
ul.body.index a {
&:active, &:link, &:visited {
color: $primary-color;
}
span:first-child {
border: 1px solid $black;
color: $white;
}
}
footer {
a {
&:active, &:link, &:visited {
color: $primary-color;
}
&:hover {
font-weight: bold;
}
}
input {
border: 1px solid $primary-color;
&:focus {
outline-color: $primary-color;
}
}
}
}

View file

@ -1,29 +0,0 @@
.root .content.theme.green {
@import "themes/green/base/colors";
@import "base/breakpoints";
header {
h1, h1 a { color: $primary-color; }
color: $primary-color;
}
ul.body.stream {
li.ayah {
color: $primary-color;
p { color: $black; }
}
}
footer {
.timer {
color: $primary-color;
}
}
.sound-on.icon, .svg.sound-off.icon {
polygon {
fill: $primary-color;
stroke-width: 2;
}
}
}

File diff suppressed because one or more lines are too long

11
src/html/index.html Normal file
View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="UTF-8">
<script src="/js/index.js"></script>
</head>
<body>
<div class="app mount root h-full"></div>
</body>
</html>

View file

@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="<%= context.locale %>" dir="<%= context.dir %>">
<head>
<title><%= t(context.locale, "TheNobleQuran") %></title>
<meta name="description" content="<%= t(context.locale, 'meta.random.description') %>">
<%= erb("_version.html.erb") %>
<link
rel="canonical"
href="<%= base_url %>/<%= context.locale %>/random/"
/>
<% context.locales.each do |locale| %>
<link
rel="alternate"
href="<%= base_url %>/<%= locale %>/random/"
hreflang="<%= locale %>" />
<% end %>
<%= erb("_favicon.html.erb") %>
</head>
<body>
<script src="/js/main/random.js?v=<%= commit %>"></script>
</body>
</html>

View file

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<title><%= t("en", "TheNobleQuran") %></title>
<meta name="description" content="<%= t('en', 'meta.index.description') %>">
<%= erb("_version.html.erb") %>
<link rel="canonical" href="<%= base_url %>/en/" />
<% locales.each do |locale| %>
<link
rel="alternate"
href="<%= base_url %>/<%= locale %>/"
hreflang="<%= locale %>" />
<% end %>
<%= erb("_favicon.html.erb") %>
</head>
<body>
<script src="/js/main/redirect.js?v=<%= commit %>"></script>
</body>
</html>

View file

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html lang="<%= context.locale %>" dir="<%= context.dir %>">
<head>
<title><%= t(context.locale, "TheNobleQuran") %></title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta charset="UTF-8">
<meta name="description" content="<%= t(context.locale, 'meta.index.description') %>">
<%= erb("_version.html.erb") %>
<link
rel="canonical"
href="<%= base_url %>/<%= context.locale %>/"
/>
<% context.locales.each do |locale| %>
<link rel="alternate"
href="<%= base_url %>/<%= locale %>/"
hreflang="<%= locale %>" />
<% end %>
<%= erb("_favicon.html.erb") %>
</head>
<body>
<div class="root h-full"></div>
<script type="text/javascript" src="/js/main/vendor.js?v=<%= commit %>"></script>
<script type="text/javascript" src="/js/main/surah-index.js?v=<%= commit %>"></script>
</body>
</html>

View file

@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="<%= context.locale %>" dir="<%= context.dir %>">
<head>
<title><%= context.surah.name %></title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8">
<meta name="description" content="<%= t(context.locale, 'meta.stream.description') %>">
<%= erb("_version.html.erb") %>
<link
rel="canonical"
href="<%= base_url %>/<%= context.locale %>/<%= context.surah.urlName %>/"
/>
<% context.locales.each do |locale| %>
<link rel="alternate"
href="<%= base_url %>/<%= locale %>/<%= context.surah.urlName %>/"
hreflang="<%= locale %>" />
<% end %>
<%= erb("_favicon.html.erb") %>
</head>
<body>
<div class="root"
data-surah-id="<%= context.surah.id %>"
data-audio-base-url="<%= audio_base_url %>">
</div>
<%= inline_json("/json/%{locale}/%{surah_id}/info.json", context:, class_name: "info") %>
<%= inline_json("/json/%{locale}/%{surah_id}/surah.json", context:, class_name: "surah") %>
<%= inline_json("/json/durations/%{surah_id}.json", context:, class_name: "durations") %>
<script src="/js/main/vendor.js?v=<%= commit %>"></script>
<script src="/js/main/surah-stream.js?v=<%= commit %>"></script>
</body>
</html>

View file

@ -8,6 +8,7 @@ type Props = {
};
export function LanguageSelect({ locale, isOpen, setIsOpen }: Props) {
const locales = Object.values(Quran.locales);
return (
<Select
value={locale.name}
@ -15,9 +16,8 @@ export function LanguageSelect({ locale, isOpen, setIsOpen }: Props) {
isOpen={isOpen}
setIsOpen={setIsOpen}
>
{Quran.locales.map((l: TLocale, i: number) => {
const path = location.pathname;
const href = path.replace(`/${locale.name}/`, `/${l.name}/`);
{locales.map((l: TLocale, i: number) => {
const href = `/${l.name}`;
return (
<Select.Option
key={i}

View file

@ -1,4 +1,5 @@
import type { ReactNode, AnchorHTMLAttributes } from "react";
import { Link } from "preact-router/match";
type Rest = AnchorHTMLAttributes<HTMLAnchorElement>;
type Props = {
value: string;
@ -6,5 +7,5 @@ type Props = {
} & Rest;
export function Option({ children, ...rest }: Props) {
return <a {...rest}>{children}</a>;
return <Link {...rest}>{children}</Link>;
}

View file

@ -4,28 +4,21 @@ import type { Theme } from "~/hooks/useTheme";
type Props = {
theme: string;
setTheme: (theme: Theme) => void;
isOpen: boolean;
setIsOpen: (b: boolean) => void;
};
export function ThemeSelect({ theme, setTheme, isOpen, setIsOpen }: Props) {
export function ThemeSelect({ theme, setTheme }: Props) {
const themes: Theme[] = ["blue", "green"];
return (
<Select
value={theme}
className="theme-select"
isOpen={isOpen}
setIsOpen={setIsOpen}
>
<Select value={theme} className="theme-select">
{themes.map((t, i) => {
return (
<Select.Option
key={i}
onClick={() => setTheme(t)}
className={classNames("block circle mb-1", t)}
className="flex justify-end w-10 h-6"
value={t}
>
<span className="block w-full h-full" />
<span className={classNames("rounded w-5 h-5", t)} />
</Select.Option>
);
})}

View file

@ -3,23 +3,16 @@ import { ThemeSelect } from "./ThemeSelect";
import { LanguageSelect } from "./LanguageSelect";
type Props = {
isOpen: boolean;
setIsOpen: (v: boolean) => void;
value: string;
children: JSX.Element[];
className?: string;
};
function Select({
value,
children: options,
className,
isOpen,
setIsOpen,
}: Props) {
function Select({ value, children: options, className }: Props) {
const [isOpen, setOpen] = useState<boolean>(false);
const [option, setOption] = useState<JSX.Element | null>(null);
const sortedOptions = options.sort((n) => (value === n.props.value ? -1 : 1));
const close = () => setIsOpen(false);
const close = () => setOpen(false);
useEffect(() => {
document.body.addEventListener("click", close);
@ -33,7 +26,7 @@ function Select({
return (
<div
className={classNames(
"react-select flex flex-col h-full relative",
"react-select flex flex-col h-full relative z-10",
className,
)}
>
@ -44,7 +37,12 @@ function Select({
<li
key={key}
className={classNames({ hidden: isHidden })}
onClick={(e) => [e.stopPropagation(), setIsOpen(!isOpen)]}
onClick={(e) => {
e.stopPropagation();
const { ref } = n.props;
setOpen(!isOpen);
ref?.current?.click();
}}
>
{n}
</li>

View file

@ -21,8 +21,8 @@ export function SurahIndex({ locale, surahs, t }: Props) {
const rootRef = useRef<HTMLDivElement>(null);
const activeEl = useMemo(
() => document.activeElement,
[document.activeElement]
)
[document.activeElement],
);
useEffect(() => {
const onKeyPress = (e) => {
@ -36,18 +36,11 @@ export function SurahIndex({ locale, surahs, t }: Props) {
return () => activeEl.removeEventListener("keydown", onKeyPress);
}, [activeEl, showLangDropdown, showThemeDropdown]);
useEffect(() => {
const el = rootRef.current;
if (el) {
el.classList.remove("invisible");
}
}, [rootRef.current, theme]);
return (
<div
ref={rootRef}
className={classNames(
"flex flex-col invisible h-full content surah-index theme",
"flex flex-col h-full content surah-index theme",
theme,
locale.name,
locale.direction,

37
src/js/index.tsx Normal file
View file

@ -0,0 +1,37 @@
import { Quran } from "Quran";
import { T } from "~/lib/t";
import { SurahIndex } from "~/components/SurahIndex";
import { render } from "preact";
import { useState, useEffect, useMemo, useRef } from "preact/hooks";
import * as React from "preact/compat";
import classNames from "classnames";
import { Router } from "preact-router";
import "core-js";
const exports = {
React,
render,
useState,
useEffect,
useMemo,
useRef,
classNames,
};
Object.assign(window, exports);
document.addEventListener("DOMContentLoaded", () => {
const Main = (function () {
const t = T(require("@json/t.json"));
return () => {
return (
<Router>
<SurahIndex path="/" locale={Quran.locales["en"]} surahs={Quran.surahs["en"]} t={t} />
<SurahIndex path="/en" locale={Quran.locales["en"]} surahs={Quran.surahs["en"]} t={t} />
<SurahIndex path="/ar" locale={Quran.locales["ar"]} surahs={Quran.surahs["ar"]} t={t} />
</Router>
);
};
})();
render(<Main />, document.querySelector(".mount"));
});

View file

@ -1,7 +0,0 @@
(function () {
const nameById = require("@json/nameById.json");
const surahId = parseInt(Math.ceil(Math.random() * 114));
const name = nameById[surahId];
const locale = location.pathname.slice(1, 3);
location.replace(["", locale, name, ""].join("/"));
})();

View file

@ -1,10 +0,0 @@
import { Quran } from "Quran";
(function () {
const defaultl = "en";
const locales = Quran.locales.map((l) => l.name);
const locale =
navigator.languages
.map((s) => s.slice(0, 2).toLowerCase())
.find((s) => locales.includes(s)) || defaultl;
location.replace(`/${locale}/`);
})();

View file

@ -1,17 +0,0 @@
import { Surah, TSurah, Quran } from "Quran";
import { T } from "~/lib/t";
import { SurahIndex } from "~/components/SurahIndex";
(function () {
const doc = document.documentElement;
const root = doc.querySelector(".root")!;
const t = T(require("@json/t.json"));
const byLocale = require("@json/surahs");
const locale = (() => {
return Quran.locales.find((ll) => ll.name === doc.lang);
})()!;
const surahs: Surah[] = byLocale[locale.name].map(
(e: TSurah) => new Surah(e),
);
render(<SurahIndex locale={locale} surahs={surahs} t={t} />, root);
})();

View file

@ -1,34 +0,0 @@
import { Quran, Surah, Ayah, TSurah } from "Quran";
import { T } from "~/lib/t";
import { SurahStream } from "~/components/SurahStream";
(function () {
const doc = document.documentElement;
const root = doc.querySelector(".root")!;
const t = T(require("@json/t.json"));
const locale = (() => {
return Quran.locales.find((ll) => ll.name === doc.lang);
})()!;
/*
* Configure an instance of Surah
*/
const node1: HTMLScriptElement = doc.querySelector(".json.info")!;
const node2: HTMLScriptElement = doc.querySelector(".json.surah")!;
const node3: HTMLScriptElement = doc.querySelector(".json.durations")!;
const blob1: TSurah = JSON.parse(node1.innerText)!;
const blob2: Array<[number, string]> = JSON.parse(node2.innerText)!;
const blob3: Array<[number, number]> = JSON.parse(node3.innerText)!;
const surah = new Surah(blob1);
for (let i = 0; i < blob2.length; i++) {
const [id, body] = blob2[i] as [number, string];
surah.ayat.push(new Ayah({ id, body }));
}
for (let i = 0; i < surah.ayat.length; i++) {
const ayah = surah.ayat[i];
const [, ms] = blob3[i];
ayah.ms = ms * 1000;
}
render(<SurahStream surah={surah} locale={locale} t={t} />, root);
})();

View file

@ -1,16 +0,0 @@
import { render } from "preact";
import { useState, useEffect, useMemo, useRef } from "preact/hooks";
import * as React from "preact/compat";
import classNames from "classnames";
import "core-js";
const exports = {
React,
render,
useState,
useEffect,
useMemo,
useRef,
classNames,
};
Object.assign(window, exports);