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 # nanoc
gem "nanoc", "~> 4.12" gem "nanoc", "~> 4.12"
gem "nanoc-webpack.rb", "~> 0.10" gem "nanoc-webpack.rb", "~> 0.10"
gem "nanoc-tidy.rb", "~> 0.8.4"
## ##
# dev # dev

View file

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

37
Rules
View file

@ -7,7 +7,6 @@ require "ryo"
require "ryo/json" require "ryo/json"
require "ryo/yaml" require "ryo/yaml"
require "nanoc-webpack" require "nanoc-webpack"
require "nanoc-tidy"
## ##
# Extensions # 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")) surahs = Ryo.from_json(path: File.join(dirs.content, "json", "surahs.json"))
tidy = `which tidy || which tidy5`.chomp tidy = `which tidy || which tidy5`.chomp
buildenv = ENV["buildenv"] || "development" 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 # Rules
passthrough "/json/durations/*.json" passthrough "/json/**/*.json"
require_rules "nanoc/rules/assets" 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, filter :webpack,
argv: %w[--config etc/webpack.vendor.js] argv: %w[--config etc/webpack.vendor.js]
write("/js/main/vendor.js") write("/js/vendor.js")
end 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 compile("/manifest.webapp") do
write("/manifest.webapp") write("/manifest.webapp")
end end
compile("/**/*") { write(nil) } compile("/**/*") { write(nil) }
layout("**/*", :erb) 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": { "dependencies": {
"@preact/compat": "^17.1.2", "@preact/compat": "^17.1.2",
"classnames": "^2.3", "classnames": "^2.3",
"preact": "^10.23.2" "preact": "^10.23.2",
"preact-router": "^4.1.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-env": "^7.25.4", "@babel/preset-env": "^7.25.4",
@ -3059,9 +3060,9 @@
} }
}, },
"node_modules/babel-loader": { "node_modules/babel-loader": {
"version": "9.1.3", "version": "9.2.1",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz",
"integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -5227,6 +5228,15 @@
"url": "https://opencollective.com/preact" "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": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -6381,6 +6391,7 @@
"license": "0BSDL", "license": "0BSDL",
"devDependencies": { "devDependencies": {
"@types/node": "^22.0", "@types/node": "^22.0",
"babel-loader": "^9.2.1",
"typescript": "^5.5" "typescript": "^5.5"
} }
} }

View file

@ -9,9 +9,10 @@
"eslint:apply": "npx eslint --config etc/eslint.config.mjs --fix src/js/" "eslint:apply": "npx eslint --config etc/eslint.config.mjs --fix src/js/"
}, },
"dependencies": { "dependencies": {
"preact": "^10.23.2",
"@preact/compat": "^17.1.2", "@preact/compat": "^17.1.2",
"classnames": "^2.3" "classnames": "^2.3",
"preact": "^10.23.2",
"preact-router": "^4.1.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-env": "^7.25.4", "@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", "name": "Quran",
"version": "0.1.0", "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", "main": "dist/index.js",
"types": ["dist/index.d.ts"], "types": [
"dist/index.d.ts"
],
"scripts": { "scripts": {
"build": "npm exec tsc", "build": "npx webpack --config etc/webpack.config.js",
"prepare": "npm run build" "prepare": "npm run build"
}, },
"repository": { "repository": {
@ -16,6 +18,7 @@
"license": "0BSDL", "license": "0BSDL",
"devDependencies": { "devDependencies": {
"@types/node": "^22.0", "@types/node": "^22.0",
"babel-loader": "^9.2.1",
"typescript": "^5.5" "typescript": "^5.5"
} }
} }

View file

@ -36,14 +36,26 @@ class Quran {
readonly surahs: Surah[]; readonly surahs: Surah[];
/** /**
* @returns {Array} The available locales * @returns {Record<string, TLocale>} The available locales
*/ */
static get locales(): TLocale[] { static get locales(): Record<string, TLocale> {
return [ return {
{"name": "en", "displayName": "English", "direction": "ltr"}, "en": {"name": "en", "displayName": "English", "direction": "ltr"},
{"name": "ar", "displayName": "العربية", "direction": "rtl"}, "ar": {"name": "ar", "displayName": "العربية", "direction": "rtl"},
{"name": "fa", "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) { constructor(self: TQuran) {

File diff suppressed because it is too large Load diff

View file

@ -2,56 +2,28 @@
** vNEXT ** vNEXT
**** Remove ~kanit-regular.ttf~ ** Add ~Quran.surahs~
This change falls back onto standard web fonts across all The ~Quran.surahs~ getter returns an object where the
languages. This should help decrease bundle size, and save key is the locale name, and the value is an array of
time processing the custom font during page load Surah objects
**** Add src/js/main/vendor.ts ** Replace the play|pause icons
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 and pause icons Replace the play and pause icons
**** Remove nanoc-gzip.rb ** Remove nanoc-gzip.rb
Unneccessary for a packaged KaiOS application Unneccessary for a packaged KaiOS application
**** Remove loaders ** Remove loaders
Remove all loaders from ~/src/js/loaders/~, and the postman Remove all loaders from ~/src/js/loaders/~, and the postman
package (~packages/typescript/postman~) package (~packages/typescript/postman~)
**** KaiOS fork ** Replace multiple entry points with a single entry point
Fork a new branch: ~kaios/main~ dedicated to KaiOS 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/~ ** Redesign as a Single Page Application (SPA)
Move a large portion of the website's configuration files to The previous approach where we generated a HTML file
the ~/etc~ directory for every surah in each language has been replaced by
a Single Page Application (SPA) that is more suitable
** v0.9.0 for KaiOS.
**** 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)

View file

@ -1,4 +1,3 @@
@import "base/colors";
@import "base/breakpoints"; @import "base/breakpoints";
@import "base/icon"; @import "base/icon";
@import "base/select"; @import "base/select";
@ -8,46 +7,111 @@ html {
height: 100%; height: 100%;
body { body {
height: 100%; height: 100%;
color: $black;
margin: 0; margin: 0;
.root { .root {
height: 100%; height: 100%;
} }
.ltr { .ltr {
font-family: 'Arial', 'Tahoma', sans-serif; font-family: "Kanit Regular";
direction: ltr; direction: ltr;
} }
.rtl { .rtl {
font-family: 'Arial', 'Tahoma', sans-serif; font-family: "Cairo Regular", "Arial", "Tahoma", sans-serif;
direction: rtl; direction: rtl;
} }
.invisible, .hidden { .invisible, .hidden {
display: none; display: none;
} }
.outline-0 { .outline-0 {
outline: 0; outline: 0;
} }
.scroll-y { .scroll-y {
overflow-y: auto; 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 { body .root .content.theme {
margin: 0 auto; 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%; width: 85%;
font-size: medium; }
header { header {
a[data-testid="h1"] { 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 * RTL languages
*/ */
body .root .content.theme.rtl { 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 */ /* max-width */
$breakpoint-kaiOS-portrait: 240px;
$breakpoint-kaiOS-landscape: 320px;
/* Standard max widths */
$breakpoint-sm: 576px; $breakpoint-sm: 576px;
$breakpoint-md: 768px; $breakpoint-md: 768px;
$breakpoint-lg: 992px; $breakpoint-lg: 992px;

View file

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

View file

@ -57,6 +57,7 @@
.refresh.icon, .refresh.icon,
.right-arrow.icon, .right-arrow.icon,
.left-arrow.icon { .left-arrow.icon {
fill: var(--primary-color);
height: 16px; height: 16px;
width: 16px; width: 16px;
} }
@ -65,6 +66,14 @@
transform: rotate(180deg); transform: rotate(180deg);
} }
.pause.icon {
g rect {
width: 15px;
height: 40px;
fill: var(--primary-color);
}
}
ul.body.stream span { ul.body.stream span {
.sound-on.icon, .sound-on.icon,
.sound-off.icon { .sound-off.icon {
@ -92,7 +101,7 @@
.root .content.theme.rtl { .root .content.theme.rtl {
.stalled.icon { .stalled.icon {
left: 0px; left: 0px;
right: 7px; right: 12px;
} }
ul.body.stream { 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 { .root .content.theme {
.green {
background: #6d765b !important;
}
.blue {
background: #3383C3 !important;
}
.react-select { .react-select {
z-index: 2;
.active { .active {
cursor: pointer; cursor: pointer;
} }
} }
.react-select.theme-select { .react-select.theme-select {
ul li,
ul li a {
-webkit-tap-highlight-color: transparent;
}
.circle { .circle {
width: 16px; background: var(--primary-color);
height: 16px;
border-radius: 12px;
} }
} }
.react-select.language-select { .react-select.language-select {
li a { 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 { body .root .content.theme {
ul.body.index { ul.body.index {
@media (max-width: $breakpoint-sm) {
li, li a { li, li a {
color: var(--accent-color);
@media (max-width: $breakpoint-sm) {
width: 100%; width: 100%;
} }
} }
@ -30,9 +31,48 @@ body .root .content.theme {
} }
footer { 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; 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 { body .root .content.theme.rtl {
ul.body.index { ul.body.index {
li a {
span:first-child {
border: 1px solid $black;
margin-left: 0.5rem;
}
}
li.surah { li.surah {
@media (max-width: $breakpoint-sm) {
width: 100%;
}
span.transliterated { display: none; } span.transliterated { display: none; }
} }
} }
@media (hover: hover) {
/* >= $breakpoint-xxl */ footer {
@media (min-width: $breakpoint-xxl) { a {
ul.body.index { &:hover {
li.surah a { font-weight: normal;
span:last-child { font-family: "Cairo Bold";
font-size: larger;
} }
} }
} }

View file

@ -16,40 +16,13 @@ body .root .content.theme {
animation: FadeIn 1s; animation: FadeIn 1s;
} }
} }
ul.body.stream:focus {
outline: none;
}
} }
body .root .content.theme.rtl { 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 { ul.body.stream {
li.ayah p { li.ayah p {
width: 100%; width: 100%;
} @extend .font-amiri;
}
}
/* >= $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;
}
} }
} }
} }

View file

@ -1,39 +1,5 @@
.root .content.theme.blue.rtl {
direction: rtl;
}
.root .content.theme.blue { .root .content.theme.blue {
@import "blue/base/colors"; --primary-color: #3383C3;
--secondary-color: #FFF;
.color-white { --accent-color: #444;
color: $white;
} }
.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 { .root .content.theme.green {
@import "green/base/colors"; --primary-color: #6d765b;
--secondary-color: #FFF;
.color-primary { --accent-color: #444;
color: $primary-color;
} }
.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) { export function LanguageSelect({ locale, isOpen, setIsOpen }: Props) {
const locales = Object.values(Quran.locales);
return ( return (
<Select <Select
value={locale.name} value={locale.name}
@ -15,9 +16,8 @@ export function LanguageSelect({ locale, isOpen, setIsOpen }: Props) {
isOpen={isOpen} isOpen={isOpen}
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
> >
{Quran.locales.map((l: TLocale, i: number) => { {locales.map((l: TLocale, i: number) => {
const path = location.pathname; const href = `/${l.name}`;
const href = path.replace(`/${locale.name}/`, `/${l.name}/`);
return ( return (
<Select.Option <Select.Option
key={i} key={i}

View file

@ -1,4 +1,5 @@
import type { ReactNode, AnchorHTMLAttributes } from "react"; import type { ReactNode, AnchorHTMLAttributes } from "react";
import { Link } from "preact-router/match";
type Rest = AnchorHTMLAttributes<HTMLAnchorElement>; type Rest = AnchorHTMLAttributes<HTMLAnchorElement>;
type Props = { type Props = {
value: string; value: string;
@ -6,5 +7,5 @@ type Props = {
} & Rest; } & Rest;
export function Option({ children, ...rest }: Props) { 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 = { type Props = {
theme: string; theme: string;
setTheme: (theme: Theme) => void; 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"]; const themes: Theme[] = ["blue", "green"];
return ( return (
<Select <Select value={theme} className="theme-select">
value={theme}
className="theme-select"
isOpen={isOpen}
setIsOpen={setIsOpen}
>
{themes.map((t, i) => { {themes.map((t, i) => {
return ( return (
<Select.Option <Select.Option
key={i} key={i}
onClick={() => setTheme(t)} onClick={() => setTheme(t)}
className={classNames("block circle mb-1", t)} className="flex justify-end w-10 h-6"
value={t} value={t}
> >
<span className="block w-full h-full" /> <span className={classNames("rounded w-5 h-5", t)} />
</Select.Option> </Select.Option>
); );
})} })}

View file

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

View file

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