Compare commits
11 Commits
345b606571
...
ab75db9a39
Author | SHA1 | Date |
---|---|---|
Robbie Antenesse | ab75db9a39 | |
Robbie Antenesse | 428af8897e | |
Robbie Antenesse | 03f65ec6b6 | |
Robbie Antenesse | e431dacd1d | |
Robbie Antenesse | a881adc667 | |
Robbie Antenesse | 140f3c5c8f | |
Robbie Antenesse | 07703f7628 | |
Robbie Antenesse | 4134976cd7 | |
Robbie Antenesse | 4bf0c9c692 | |
Robbie Antenesse | b4d9635d15 | |
Robbie Antenesse | 11167a639b |
|
@ -25,9 +25,3 @@ It's less useful, but `npm run serve-frontend-only` will bundle and serve _only_
|
||||||
## Production
|
## Production
|
||||||
|
|
||||||
`npm run bundle` bundles and minifies the frontend stuff and also copies the backend stuff to `dist`. Be sure to run `npm run clear` to delete the contents of `dist` and `.cache` before using `npm run bundle` to make sure you don't get old dev versions of the bundled code included in your upload.
|
`npm run bundle` bundles and minifies the frontend stuff and also copies the backend stuff to `dist`. Be sure to run `npm run clear` to delete the contents of `dist` and `.cache` before using `npm run bundle` to make sure you don't get old dev versions of the bundled code included in your upload.
|
||||||
|
|
||||||
## UpUp Configuration
|
|
||||||
|
|
||||||
[UpUp](https://github.com/TalAter/UpUp) is a tool that enables browsers to download an offline version of a website so users can still access it if they lose internet connection. Because Parcel Bundler hashes every file accessed via reference within the code, you need to ensure that the UpUp configuration at the bottom of `index.html` is kept up to date whenever you make changes to files. Typically the only file hashes that will change are `src.*.js` and `main.*.css`, but it's best to check all of them just to make sure.
|
|
||||||
|
|
||||||
After bundling, update the files referenced in the configuration to make sure UpUp can download the files correctly, then bundle again so `dist/index.html` gets updated. I'm desperately hoping I can find a way to automate this in the build process, but I haven't figured it out just yet. Maybe I'll end up using `router.php` and `.htaccess` to do the heavy lifting for me. We'll see.
|
|
12
package.json
12
package.json
|
@ -2,17 +2,19 @@
|
||||||
"name": "lexiconga-lite",
|
"name": "lexiconga-lite",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "A light-as-possible rewrite of Lexiconga",
|
"description": "A light-as-possible rewrite of Lexiconga",
|
||||||
"main": "index.html",
|
"main": "template-index.html",
|
||||||
"repository": "https://cybre.tech/Alamantus/lexiconga-lite.git",
|
"repository": "https://cybre.tech/Alamantus/lexiconga-lite.git",
|
||||||
"author": "Robbie Antenesse <dev@alamantus.com>",
|
"author": "Robbie Antenesse <dev@alamantus.com>",
|
||||||
"license": "UNLICENCED",
|
"license": "UNLICENCED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "concurrently \"npm run watch-js\" \"npm run watch-php\" \"npm run copy-files\"",
|
"start": "concurrently \"npm run watch-js\" \"npm run watch-php\" \"npm run copy-files\"",
|
||||||
"watch-js": "parcel watch index.html offline.html template-view.html template-passwordreset.html --no-hmr --public-url ./",
|
"watch-js": "parcel watch template-index.html offline.html template-view.html template-passwordreset.html --no-hmr --public-url ./",
|
||||||
"watch-php": "cpx \"src/php/**/{*,.*}\" dist -v -w",
|
"watch-php": "cpx \"src/php/**/{*,.*}\" dist -v -w",
|
||||||
"copy-files": "cpx \"node_modules/upup/dist/upup.sw.min.js\" dist -v",
|
"bundle": "npm run bundle-js && npm run copy-files && npm run copy-php",
|
||||||
"bundle": "parcel build index.html offline.html template-view.html template-passwordreset.html && npm run copy-files && cpx \"src/php/**/{*,.*}\" dist",
|
"bundle-js": "parcel build template-index.html offline.html template-view.html template-passwordreset.html",
|
||||||
"serve-frontend-only": "parcel index.html",
|
"copy-files": "cpx \"node_modules/upup/dist/*.min.js\" dist -v",
|
||||||
|
"copy-php": "cpx \"src/php/**/{*,.*}\" dist",
|
||||||
|
"serve-frontend-only": "parcel template-index.html",
|
||||||
"clear": "npm run clear-dist && npm run clear-cache",
|
"clear": "npm run clear-dist && npm run clear-cache",
|
||||||
"clear-dist": "rimraf dist/*",
|
"clear-dist": "rimraf dist/*",
|
||||||
"clear-cache": "rimraf .cache/*"
|
"clear-cache": "rimraf .cache/*"
|
||||||
|
|
|
@ -20,12 +20,11 @@ function initialize() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setupAds().then(() => renderAll());
|
setupAds();
|
||||||
|
renderAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.onload = (function (oldLoad) {
|
window.onload = (function (oldLoad) {
|
||||||
return function () {
|
oldLoad && oldLoad();
|
||||||
oldLoad && oldLoad();
|
initialize();
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
})(window.onload);
|
})(window.onload);
|
|
@ -1,4 +1,3 @@
|
||||||
import { setupInfoModal } from "../setupListeners";
|
|
||||||
import { request } from "./helpers";
|
import { request } from "./helpers";
|
||||||
|
|
||||||
export function renderForgotPasswordForm() {
|
export function renderForgotPasswordForm() {
|
||||||
|
@ -24,6 +23,15 @@ export function renderForgotPasswordForm() {
|
||||||
setupInfoModal(modal);
|
setupInfoModal(modal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupInfoModal(modal) {
|
||||||
|
const closeElements = modal.querySelectorAll('.modal-background, .close-button');
|
||||||
|
Array.from(closeElements).forEach(close => {
|
||||||
|
close.addEventListener('click', () => {
|
||||||
|
modal.parentElement.removeChild(modal);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function setupStartResetForm() {
|
function setupStartResetForm() {
|
||||||
document.getElementById('forgotPasswordSubmit').addEventListener('click', startPasswordReset);
|
document.getElementById('forgotPasswordSubmit').addEventListener('click', startPasswordReset);
|
||||||
}
|
}
|
||||||
|
@ -59,7 +67,10 @@ function startPasswordReset() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupPasswordResetForm() {
|
function setupPasswordResetForm() {
|
||||||
document.getElementById('newPasswordSubmit').addEventListener('click', submitPasswordReset);
|
const submitButton = document.getElementById('newPasswordSubmit');
|
||||||
|
if (submitButton) {
|
||||||
|
submitButton.addEventListener('click', submitPasswordReset);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitPasswordReset() {
|
function submitPasswordReset() {
|
||||||
|
@ -97,8 +108,6 @@ function submitPasswordReset() {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.onload = (function (oldLoad) {
|
window.onload = (function (oldLoad) {
|
||||||
return function () {
|
oldLoad && oldLoad();
|
||||||
oldLoad && oldLoad();
|
setupPasswordResetForm();
|
||||||
setupPasswordResetForm();
|
|
||||||
}
|
|
||||||
})(window.onload);
|
})(window.onload);
|
|
@ -1,41 +1,14 @@
|
||||||
import { addMessage } from "../utilities";
|
import { addMessage } from "../utilities";
|
||||||
import { saveDictionary, clearDictionary } from "../dictionaryManagement";
|
import { saveDictionary, clearDictionary } from "../dictionaryManagement";
|
||||||
import { request } from "./helpers";
|
import { request } from "./helpers";
|
||||||
import { saveToken } from "./utilities";
|
import { saveToken, dictionaryIsDefault } from "./utilities";
|
||||||
import { renderAll } from "../render";
|
import { renderAll } from "../render";
|
||||||
import { sortWords } from "../wordManagement";
|
import { sortWords } from "../wordManagement";
|
||||||
import { getLocalDeletedWords, clearLocalDeletedWords, saveDeletedWordsLocally } from "./utilities";
|
import { getLocalDeletedWords, clearLocalDeletedWords, saveDeletedWordsLocally } from "./utilities";
|
||||||
import { renderChangeDictionaryOptions } from "./render";
|
import { renderChangeDictionaryOptions } from "./render";
|
||||||
|
|
||||||
/* Outline for syncing
|
|
||||||
login
|
|
||||||
-> check local dictionary id
|
|
||||||
(DONE!) ? no id
|
|
||||||
-> upload dictionary
|
|
||||||
-> make new dictionary current
|
|
||||||
(Canceled) ? mismatched id
|
|
||||||
-> sync local dictionary (see 'same id' below)
|
|
||||||
-> if no matching remote id, ignore (assume deleted)
|
|
||||||
-> clear local dictionary
|
|
||||||
-> insert downloaded dictionary
|
|
||||||
(DONE!) ? same id
|
|
||||||
-> compare detail last updated timestamp
|
|
||||||
? downloaded details are newer
|
|
||||||
-> replace local details
|
|
||||||
? local details are newer
|
|
||||||
-> flag to upload details
|
|
||||||
-> filter deleted words from current words
|
|
||||||
-- check id and compare deletedOn with createdOn
|
|
||||||
-> compare each word and by lastUpdated/createdOn
|
|
||||||
? downloaded word is newer
|
|
||||||
-> update local word
|
|
||||||
? local word is newer
|
|
||||||
-> put word in an array to upload
|
|
||||||
-> upload anything that needs update
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function syncDictionary(uploadAsNewIfNoExternalID = true) {
|
export function syncDictionary(uploadAsNewIfNoExternalID = true) {
|
||||||
if (!window.currentDictionary.hasOwnProperty('externalID')) {
|
if (!window.currentDictionary.hasOwnProperty('externalID') && !dictionaryIsDefault()) {
|
||||||
uploadWholeDictionary(uploadAsNewIfNoExternalID);
|
uploadWholeDictionary(uploadAsNewIfNoExternalID);
|
||||||
} else {
|
} else {
|
||||||
addMessage('Syncing...');
|
addMessage('Syncing...');
|
||||||
|
|
|
@ -1,11 +1,25 @@
|
||||||
import { setCookie } from "../StackOverflow/cookie";
|
import { setCookie } from "../StackOverflow/cookie";
|
||||||
import { DELETED_WORDS_LOCALSTORAGE_KEY } from "./constants";
|
import { DELETED_WORDS_LOCALSTORAGE_KEY } from "./constants";
|
||||||
import { getTimestampInSeconds } from "../../helpers";
|
import { getTimestampInSeconds, cloneObject } from "../../helpers";
|
||||||
|
import { DEFAULT_DICTIONARY } from "../../constants";
|
||||||
|
|
||||||
export function saveToken(token) {
|
export function saveToken(token) {
|
||||||
setCookie('token', token, 30);
|
setCookie('token', token, 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function dictionaryIsDefault() {
|
||||||
|
const defaultDictionary = cloneObject(DEFAULT_DICTIONARY);
|
||||||
|
delete defaultDictionary.lastUpdated;
|
||||||
|
delete defaultDictionary.createdOn;
|
||||||
|
delete defaultDictionary.version;
|
||||||
|
const currentDictionary = cloneObject(window.currentDictionary);
|
||||||
|
delete currentDictionary.lastUpdated;
|
||||||
|
delete currentDictionary.createdOn;
|
||||||
|
delete currentDictionary.version;
|
||||||
|
console.log(JSON.stringify(defaultDictionary) === JSON.stringify(currentDictionary));
|
||||||
|
return JSON.stringify(defaultDictionary) === JSON.stringify(currentDictionary);
|
||||||
|
}
|
||||||
|
|
||||||
export function saveDeletedWordsLocally(wordIds) {
|
export function saveDeletedWordsLocally(wordIds) {
|
||||||
let storedDeletedWords = getLocalDeletedWords();
|
let storedDeletedWords = getLocalDeletedWords();
|
||||||
wordIds.forEach(wordId => {
|
wordIds.forEach(wordId => {
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { DISPLAY_AD_EVERY } from '../constants.js';
|
import { DISPLAY_AD_EVERY } from '../constants.js';
|
||||||
|
import ads from '../../ads.json';
|
||||||
|
|
||||||
export function setupAds() {
|
export function setupAds() {
|
||||||
return import('../../ads.json').then(ads => {
|
const shuffle = (a, b) => Math.random() > 0.5 ? 1 : -1;
|
||||||
const shuffle = (a, b) => Math.random() > 0.5 ? 1 : -1;
|
const priority = ads.filter(ad => isActive(ad) && ad.isPriority).sort(shuffle);
|
||||||
const priority = ads.filter(ad => isActive(ad) && ad.isPriority).sort(shuffle);
|
const regular = ads.filter(ad => isActive(ad) && !ad.isPriority).sort(shuffle);
|
||||||
const regular = ads.filter(ad => isActive(ad) && !ad.isPriority).sort(shuffle);
|
window.ads = [...priority, ...regular];
|
||||||
window.ads = [...priority, ...regular];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isActive(ad) {
|
function isActive(ad) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import papa from 'papaparse';
|
||||||
import { renderDictionaryDetails, renderPartsOfSpeech, renderAll, renderTheme } from "./render";
|
import { renderDictionaryDetails, renderPartsOfSpeech, renderAll, renderTheme } from "./render";
|
||||||
import { removeTags, cloneObject, getTimestampInSeconds, download, slugify } from "../helpers";
|
import { removeTags, cloneObject, getTimestampInSeconds, download, slugify } from "../helpers";
|
||||||
import { LOCAL_STORAGE_KEY, DEFAULT_DICTIONARY, MIGRATE_VERSION } from "../constants";
|
import { LOCAL_STORAGE_KEY, DEFAULT_DICTIONARY, MIGRATE_VERSION } from "../constants";
|
||||||
|
@ -203,50 +204,48 @@ export function importWords() {
|
||||||
if (importWordsField.files.length === 1) {
|
if (importWordsField.files.length === 1) {
|
||||||
if (confirm('Importing a CSV file with words will add all of the words in the file to your dictionary regardless of duplication!\nDo you want to continue?')) {
|
if (confirm('Importing a CSV file with words will add all of the words in the file to your dictionary regardless of duplication!\nDo you want to continue?')) {
|
||||||
addMessage('Importing words...');
|
addMessage('Importing words...');
|
||||||
import('papaparse').then(papa => {
|
const importedWords = [];
|
||||||
const importedWords = [];
|
papa.parse(importWordsField.files[0], {
|
||||||
papa.parse(importWordsField.files[0], {
|
header: true,
|
||||||
header: true,
|
encoding: "utf-8",
|
||||||
encoding: "utf-8",
|
step: results => {
|
||||||
step: results => {
|
if (results.errors.length > 0) {
|
||||||
if (results.errors.length > 0) {
|
results.errors.forEach(err => {
|
||||||
results.errors.forEach(err => {
|
addMessage('Error Importing Word: ' + err, undefined, 'error');
|
||||||
addMessage('Error Importing Word: ' + err, undefined, 'error');
|
console.error('Error Importing Word: ', err)
|
||||||
console.error('Error Importing Word: ', err)
|
});
|
||||||
});
|
} else {
|
||||||
} else {
|
const row = results.data[0];
|
||||||
const row = results.data[0];
|
const importedWord = addWord({
|
||||||
const importedWord = addWord({
|
name: removeTags(row.word).trim(),
|
||||||
name: removeTags(row.word).trim(),
|
pronunciation: removeTags(row.pronunciation).trim(),
|
||||||
pronunciation: removeTags(row.pronunciation).trim(),
|
partOfSpeech: removeTags(row['part of speech']).trim(),
|
||||||
partOfSpeech: removeTags(row['part of speech']).trim(),
|
definition: removeTags(row.definition).trim(),
|
||||||
definition: removeTags(row.definition).trim(),
|
details: removeTags(row.explanation).trim(),
|
||||||
details: removeTags(row.explanation).trim(),
|
wordId: getNextId(),
|
||||||
wordId: getNextId(),
|
}, false, false, false);
|
||||||
}, false, false, false);
|
|
||||||
|
|
||||||
importedWords.push(importedWord);
|
importedWords.push(importedWord);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
saveDictionary(false);
|
saveDictionary(false);
|
||||||
renderAll();
|
renderAll();
|
||||||
importWordsField.value = '';
|
importWordsField.value = '';
|
||||||
document.getElementById('editModal').style.display = 'none';
|
document.getElementById('editModal').style.display = 'none';
|
||||||
addMessage(`Done Importing ${importedWords.length} Words`);
|
addMessage(`Done Importing ${importedWords.length} Words`);
|
||||||
|
|
||||||
if (hasToken()) {
|
if (hasToken()) {
|
||||||
import('./account/index.js').then(account => {
|
import('./account/index.js').then(account => {
|
||||||
account.syncImportedWords(importedWords);
|
account.syncImportedWords(importedWords);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: err => {
|
error: err => {
|
||||||
addMessage('Error Importing Words: ' + err, undefined, 'error');
|
addMessage('Error Importing Words: ' + err, undefined, 'error');
|
||||||
console.error('Error Importing Words: ', err);
|
console.error('Error Importing Words: ', err);
|
||||||
},
|
},
|
||||||
skipEmptyLines: true,
|
skipEmptyLines: true,
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -269,23 +268,21 @@ export function exportWords() {
|
||||||
addMessage('Exporting Words...');
|
addMessage('Exporting Words...');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
import('papaparse').then(papa => {
|
const { name, specification } = window.currentDictionary;
|
||||||
const { name, specification } = window.currentDictionary;
|
|
||||||
|
|
||||||
const fileName = slugify(name + '_' + specification) + '_words.csv';
|
const fileName = slugify(name + '_' + specification) + '_words.csv';
|
||||||
|
|
||||||
const words = window.currentDictionary.words.map(word => {
|
const words = window.currentDictionary.words.map(word => {
|
||||||
return {
|
return {
|
||||||
word: word.name,
|
word: word.name,
|
||||||
pronunciation: word.pronunciation,
|
pronunciation: word.pronunciation,
|
||||||
'part of speech': word.partOfSpeech,
|
'part of speech': word.partOfSpeech,
|
||||||
definition: word.definition,
|
definition: word.definition,
|
||||||
explanation: word.details,
|
explanation: word.details,
|
||||||
}
|
}
|
||||||
});
|
|
||||||
const csv = papa.unparse(words, { quotes: true });
|
|
||||||
download(csv, fileName, 'text/csv;charset=utf-8');
|
|
||||||
});
|
});
|
||||||
|
const csv = papa.unparse(words, { quotes: true });
|
||||||
|
download(csv, fileName, 'text/csv;charset=utf-8');
|
||||||
}, 1);
|
}, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { showSearchModal, clearSearchText } from "./search";
|
||||||
import { saveAndCloseSettingsModal, openSettingsModal, saveSettings } from "./settings";
|
import { saveAndCloseSettingsModal, openSettingsModal, saveSettings } from "./settings";
|
||||||
import { saveAndCloseEditModal, openEditModal } from "./dictionaryManagement";
|
import { saveAndCloseEditModal, openEditModal } from "./dictionaryManagement";
|
||||||
import { addMessage, hideAllModals } from "./utilities";
|
import { addMessage, hideAllModals } from "./utilities";
|
||||||
|
import helpFile from '../markdown/help.md';
|
||||||
|
|
||||||
export function enableHotKeys() {
|
export function enableHotKeys() {
|
||||||
document.addEventListener('keydown', hotKeyActions);
|
document.addEventListener('keydown', hotKeyActions);
|
||||||
|
@ -102,9 +103,7 @@ function submitWord() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function showHelpModal() {
|
function showHelpModal() {
|
||||||
import('../markdown/help.md').then(html => {
|
renderInfoModal(helpFile);
|
||||||
renderInfoModal(html);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function maximizeTextarea() {
|
function maximizeTextarea() {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
import { getPaginationData } from './pagination';
|
import { getPaginationData } from './pagination';
|
||||||
import { getOpenEditForms, parseReferences } from './wordManagement';
|
import { getOpenEditForms, parseReferences } from './wordManagement';
|
||||||
import { renderAd } from './ads';
|
import { renderAd } from './ads';
|
||||||
|
import ipaTableFile from './KeyboardFire/phondue/ipa-table.html';
|
||||||
|
|
||||||
export function renderAll() {
|
export function renderAll() {
|
||||||
renderTheme();
|
renderTheme();
|
||||||
|
@ -267,18 +268,16 @@ export function renderEditForm(wordId = false) {
|
||||||
wordId = typeof wordId.target === 'undefined' ? wordId : parseInt(this.id.replace('edit_', ''));
|
wordId = typeof wordId.target === 'undefined' ? wordId : parseInt(this.id.replace('edit_', ''));
|
||||||
const word = window.currentDictionary.words.find(w => w.wordId === wordId);
|
const word = window.currentDictionary.words.find(w => w.wordId === wordId);
|
||||||
if (word) {
|
if (word) {
|
||||||
const ipaPronunciationField = `<label>Pronunciation<a class="label-button ipa-table-button">IPA Chart</a><br>
|
const ipaPronunciationField = `<input id="wordPronunciation_${wordId}" class="ipa-field" maxlength="200" value="${word.pronunciation}"><br>
|
||||||
<input id="wordPronunciation_${wordId}" class="ipa-field" maxlength="200" value="${word.pronunciation}"><br>
|
<a class="label-help-button ipa-field-help-button">Field Help</a>`;
|
||||||
<a class="label-help-button ipa-field-help-button">Field Help</a>
|
const plainPronunciationField = `<input id="wordPronunciation_${wordId}" maxlength="200" value="${word.pronunciation}">`;
|
||||||
</label>`;
|
|
||||||
const plainPronunciationField = `<label>Pronunciation<br>
|
|
||||||
<input id="wordPronunciation_${wordId}" maxlength="200" value="${word.pronunciation}">
|
|
||||||
</label>`;
|
|
||||||
const editForm = `<form id="editForm_${wordId}" class="edit-form">
|
const editForm = `<form id="editForm_${wordId}" class="edit-form">
|
||||||
<label>Word<span class="red">*</span><br>
|
<label>Word<span class="red">*</span><br>
|
||||||
<input id="wordName_${wordId}" maxlength="200" value="${word.name}">
|
<input id="wordName_${wordId}" maxlength="200" value="${word.name}">
|
||||||
</label>
|
</label>
|
||||||
${window.settings.useIPAPronunciationField ? ipaPronunciationField : plainPronunciationField}
|
<label>Pronunciation<a class="label-button ipa-table-button">IPA Chart</a><br>
|
||||||
|
${window.settings.useIPAPronunciationField ? ipaPronunciationField : plainPronunciationField}
|
||||||
|
</label>
|
||||||
<label>Part of Speech<br>
|
<label>Part of Speech<br>
|
||||||
<select id="wordPartOfSpeech_${wordId}" class="part-of-speech-select">
|
<select id="wordPartOfSpeech_${wordId}" class="part-of-speech-select">
|
||||||
<option value="${word.partOfSpeech}" selected>${word.partOfSpeech}</option>
|
<option value="${word.partOfSpeech}" selected>${word.partOfSpeech}</option>
|
||||||
|
@ -311,24 +310,22 @@ export function renderIPATable(ipaTableButton) {
|
||||||
ipaTableButton = typeof ipaTableButton.target === 'undefined' || ipaTableButton.target === '' ? ipaTableButton : ipaTableButton.target;
|
ipaTableButton = typeof ipaTableButton.target === 'undefined' || ipaTableButton.target === '' ? ipaTableButton : ipaTableButton.target;
|
||||||
const label = ipaTableButton.parentElement.innerText.replace(/(Field Help|IPA Chart)/g, '').trim();
|
const label = ipaTableButton.parentElement.innerText.replace(/(Field Help|IPA Chart)/g, '').trim();
|
||||||
const textBox = ipaTableButton.parentElement.querySelector('input');
|
const textBox = ipaTableButton.parentElement.querySelector('input');
|
||||||
import('./KeyboardFire/phondue/ipa-table.html').then(html => {
|
const modalElement = document.createElement('section');
|
||||||
const modalElement = document.createElement('section');
|
modalElement.classList.add('modal', 'ipa-table-modal');
|
||||||
modalElement.classList.add('modal', 'ipa-table-modal');
|
modalElement.innerHTML = `<div class="modal-background"></div>
|
||||||
modalElement.innerHTML = `<div class="modal-background"></div>
|
<div class="modal-content">
|
||||||
<div class="modal-content">
|
<a class="close-button">×︎</a>
|
||||||
<a class="close-button">×︎</a>
|
<header><label>${label} <input value="${textBox.value}" class="ipa-field"></label></header>
|
||||||
<header><label>${label} <input value="${textBox.value}" class="ipa-field"></label></header>
|
<section>
|
||||||
<section>
|
${ipaTableFile}
|
||||||
${html}
|
</section>
|
||||||
</section>
|
<footer><a class="button done-button">Done</a></footer>
|
||||||
<footer><a class="button done-button">Done</a></footer>
|
</div>`;
|
||||||
</div>`;
|
|
||||||
|
|
||||||
document.body.appendChild(modalElement);
|
document.body.appendChild(modalElement);
|
||||||
|
|
||||||
setupIPAFields();
|
setupIPAFields();
|
||||||
setupIPATable(modalElement, textBox);
|
setupIPATable(modalElement, textBox);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderMaximizedTextbox(maximizeButton) {
|
export function renderMaximizedTextbox(maximizeButton) {
|
||||||
|
|
|
@ -68,7 +68,7 @@ export function toggleHotkeysEnabled() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleIPAPronunciationFields(render = true) {
|
export function toggleIPAPronunciationFields(render = true) {
|
||||||
const ipaButtons = document.querySelectorAll('.ipa-table-button, .ipa-field-help-button'),
|
const ipaButtons = document.querySelectorAll('.ipa-field-help-button'),
|
||||||
ipaFields = document.querySelectorAll('.ipa-field');
|
ipaFields = document.querySelectorAll('.ipa-field');
|
||||||
if (!window.settings.useIPAPronunciationField) {
|
if (!window.settings.useIPAPronunciationField) {
|
||||||
Array.from(ipaButtons).forEach(button => {
|
Array.from(ipaButtons).forEach(button => {
|
||||||
|
|
|
@ -8,6 +8,9 @@ import { usePhondueDigraphs } from './KeyboardFire/phondue/ipaField';
|
||||||
import { openSettingsModal, saveSettingsModal, saveAndCloseSettingsModal } from './settings';
|
import { openSettingsModal, saveSettingsModal, saveAndCloseSettingsModal } from './settings';
|
||||||
import { enableHotKeys } from './hotkeys';
|
import { enableHotKeys } from './hotkeys';
|
||||||
import { showSearchModal, clearSearchText, checkAllPartsOfSpeechFilters, uncheckAllPartsOfSpeechFilters } from './search';
|
import { showSearchModal, clearSearchText, checkAllPartsOfSpeechFilters, uncheckAllPartsOfSpeechFilters } from './search';
|
||||||
|
import helpFile from '../markdown/help.md';
|
||||||
|
import termsFile from '../markdown/terms.md';
|
||||||
|
import privacyFile from '../markdown/privacy.md';
|
||||||
|
|
||||||
export default function setupListeners() {
|
export default function setupListeners() {
|
||||||
setupDetailsTabs();
|
setupDetailsTabs();
|
||||||
|
@ -357,19 +360,13 @@ export function setupMaximizeModal(modal, textBox) {
|
||||||
|
|
||||||
export function setupInfoButtons() {
|
export function setupInfoButtons() {
|
||||||
document.getElementById('helpInfoButton').addEventListener('click', () => {
|
document.getElementById('helpInfoButton').addEventListener('click', () => {
|
||||||
import('../markdown/help.md').then(html => {
|
renderInfoModal(helpFile);
|
||||||
renderInfoModal(html);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
document.getElementById('termsInfoButton').addEventListener('click', () => {
|
document.getElementById('termsInfoButton').addEventListener('click', () => {
|
||||||
import('../markdown/terms.md').then(html => {
|
renderInfoModal(termsFile);
|
||||||
renderInfoModal(html);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
document.getElementById('privacyInfoButton').addEventListener('click', () => {
|
document.getElementById('privacyInfoButton').addEventListener('click', () => {
|
||||||
import('../markdown/privacy.md').then(html => {
|
renderInfoModal(privacyFile);
|
||||||
renderInfoModal(html);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
export function getDictionary() {
|
|
||||||
const url = window.location.href.replace(/\#.*$/gi, '');
|
|
||||||
console.log(url);
|
|
||||||
let dict = url.substr(url.lastIndexOf('?'));
|
|
||||||
console.log(dict);
|
|
||||||
if (dict === url) {
|
|
||||||
dict = dict.substr(dict.lastIndexOf('/'));
|
|
||||||
console.log(dict);
|
|
||||||
}
|
|
||||||
dict = dict.replace(/[\?\/]/g, '');
|
|
||||||
console.log(dict);
|
|
||||||
}
|
|
|
@ -2,20 +2,13 @@ import { renderAll } from './render';
|
||||||
import setupListeners from './setupListeners';
|
import setupListeners from './setupListeners';
|
||||||
import { setupAds } from '../ads';
|
import { setupAds } from '../ads';
|
||||||
|
|
||||||
// import setupListeners, { setupSearchFilters } from './js/setupListeners';
|
|
||||||
// import { renderAll } from './js/render';
|
|
||||||
// import { hasToken } from './js/utilities';
|
|
||||||
// import { loadDictionary } from './js/dictionaryManagement';
|
|
||||||
// import { loadSettings } from './js/settings';
|
|
||||||
|
|
||||||
function initialize() {
|
function initialize() {
|
||||||
setupAds().then(() => renderAll());
|
setupAds();
|
||||||
|
renderAll();
|
||||||
setupListeners();
|
setupListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.onload = (function (oldLoad) {
|
window.onload = (function (oldLoad) {
|
||||||
return function () {
|
oldLoad && oldLoad();
|
||||||
oldLoad && oldLoad();
|
initialize();
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
})(window.onload);
|
})(window.onload);
|
|
@ -1,20 +1,26 @@
|
||||||
import md from 'marked';
|
import md from 'marked';
|
||||||
import { removeTags, slugify } from '../../helpers';
|
import { removeTags, slugify } from '../../helpers';
|
||||||
import { getWordsStats, wordExists } from '../utilities';
|
import { getWordsStats, getHomonymnNumber } from './utilities';
|
||||||
import { getMatchingSearchWords, highlightSearchTerm, getSearchFilters, getSearchTerm } from '../search';
|
import { getMatchingSearchWords, highlightSearchTerm, getSearchFilters, getSearchTerm } from './search';
|
||||||
import { showSection } from '../displayToggles';
|
import { showSection } from './displayToggles';
|
||||||
import { setupSearchFilters, setupInfoModal } from './setupListeners';
|
import { setupSearchFilters, setupInfoModal } from './setupListeners';
|
||||||
import { parseReferences } from '../wordManagement';
|
import { parseReferences } from './wordManagement';
|
||||||
import { renderTheme } from '../render';
|
|
||||||
import { renderAd } from '../ads';
|
import { renderAd } from '../ads';
|
||||||
|
import { sortWords } from './wordManagement';
|
||||||
|
|
||||||
export function renderAll() {
|
export function renderAll() {
|
||||||
renderTheme();
|
renderTheme();
|
||||||
renderDictionaryDetails();
|
renderDictionaryDetails();
|
||||||
renderPartsOfSpeech();
|
renderPartsOfSpeech();
|
||||||
|
sortWords();
|
||||||
renderWords();
|
renderWords();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function renderTheme() {
|
||||||
|
const { theme } = window.currentDictionary.settings;
|
||||||
|
document.body.id = theme + 'Theme';
|
||||||
|
}
|
||||||
|
|
||||||
export function renderDictionaryDetails() {
|
export function renderDictionaryDetails() {
|
||||||
renderName();
|
renderName();
|
||||||
|
|
||||||
|
@ -149,13 +155,15 @@ export function renderWords() {
|
||||||
details: parseReferences(removeTags(originalWord.details)),
|
details: parseReferences(removeTags(originalWord.details)),
|
||||||
wordId: originalWord.wordId,
|
wordId: originalWord.wordId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const homonymnNumber = getHomonymnNumber(originalWord);
|
||||||
const shareLink = window.location.pathname + (window.location.pathname.match(new RegExp(word.wordId + '$')) ? '' : '/' + word.wordId);
|
const shareLink = window.location.pathname + (window.location.pathname.match(new RegExp(word.wordId + '$')) ? '' : '/' + word.wordId);
|
||||||
|
|
||||||
wordsHTML += renderAd(displayIndex);
|
wordsHTML += renderAd(displayIndex);
|
||||||
|
|
||||||
wordsHTML += `<article class="entry" id="${word.wordId}">
|
wordsHTML += `<article class="entry" id="${word.wordId}">
|
||||||
<header>
|
<header>
|
||||||
<h4 class="word">${word.name}</h4>
|
<h4 class="word">${word.name}${homonymnNumber > 0 ? ' <sub>' + homonymnNumber.toString() + '</sub>' : ''}</h4>
|
||||||
<span class="pronunciation">${word.pronunciation}</span>
|
<span class="pronunciation">${word.pronunciation}</span>
|
||||||
<span class="part-of-speech">${word.partOfSpeech}</span>
|
<span class="part-of-speech">${word.partOfSpeech}</span>
|
||||||
<a href="${shareLink}" target="_blank" class="small button word-option-button" title="Link to Word">➦</a>
|
<a href="${shareLink}" target="_blank" class="small button word-option-button" title="Link to Word">➦</a>
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
import { cloneObject, getIndicesOf } from "../../helpers";
|
||||||
|
import removeDiacritics from "../StackOverflow/removeDiacritics";
|
||||||
|
import { renderWords } from "./render";
|
||||||
|
|
||||||
|
export function showSearchModal() {
|
||||||
|
document.getElementById('searchModal').style.display = 'block';
|
||||||
|
document.getElementById('searchBox').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearSearchText() {
|
||||||
|
document.getElementById('searchBox').value = '';
|
||||||
|
document.getElementById('openSearchModal').value = '';
|
||||||
|
renderWords();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSearchTerm() {
|
||||||
|
return document.getElementById('searchBox').value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSearchFilters() {
|
||||||
|
const filters = {
|
||||||
|
caseSensitive: document.getElementById('searchCaseSensitive').checked,
|
||||||
|
ignoreDiacritics: document.getElementById('searchIgnoreDiacritics').checked,
|
||||||
|
exact: document.getElementById('searchExactWords').checked,
|
||||||
|
name: document.getElementById('searchIncludeName').checked,
|
||||||
|
definition: document.getElementById('searchIncludeDefinition').checked,
|
||||||
|
details: document.getElementById('searchIncludeDetails').checked,
|
||||||
|
partsOfSpeech: {},
|
||||||
|
};
|
||||||
|
const partsOfSpeech = document.querySelectorAll('#searchPartsOfSpeech input[type="checkbox"]');
|
||||||
|
let checkedBoxes = 0;
|
||||||
|
Array.from(partsOfSpeech).forEach(partOfSpeech => {
|
||||||
|
// console.log('partOfSpeech Inner Text:', partOfSpeech.parentElement.innerText);
|
||||||
|
const partOfSpeechLabel = partOfSpeech.parentElement.innerText.trim();
|
||||||
|
filters.partsOfSpeech[partOfSpeechLabel] = partOfSpeech.checked;
|
||||||
|
if (partOfSpeech.checked) checkedBoxes++;
|
||||||
|
});
|
||||||
|
|
||||||
|
filters.allPartsOfSpeechChecked = checkedBoxes === partsOfSpeech.length;
|
||||||
|
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMatchingSearchWords() {
|
||||||
|
let searchTerm = getSearchTerm();
|
||||||
|
const filters = getSearchFilters();
|
||||||
|
if (searchTerm !== '' || !filters.allPartsOfSpeechChecked) {
|
||||||
|
const matchingWords = window.currentDictionary.words.slice().filter(word => {
|
||||||
|
if (!filters.allPartsOfSpeechChecked) {
|
||||||
|
const partOfSpeech = word.partOfSpeech === '' ? 'Unclassified' : word.partOfSpeech;
|
||||||
|
return filters.partsOfSpeech.hasOwnProperty(partOfSpeech) && filters.partsOfSpeech[partOfSpeech];
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).filter(word => {
|
||||||
|
searchTerm = filters.ignoreDiacritics ? removeDiacritics(searchTerm) : searchTerm;
|
||||||
|
searchTerm = filters.caseSensitive ? searchTerm : searchTerm.toLowerCase();
|
||||||
|
let name = filters.ignoreDiacritics ? removeDiacritics(word.name) : word.name;
|
||||||
|
name = filters.caseSensitive ? name : name.toLowerCase();
|
||||||
|
let definition = filters.ignoreDiacritics ? removeDiacritics(word.definition) : word.definition;
|
||||||
|
definition = filters.caseSensitive ? definition : definition.toLowerCase();
|
||||||
|
let details = filters.ignoreDiacritics ? removeDiacritics(word.details) : word.details;
|
||||||
|
details = filters.caseSensitive ? details : details.toLowerCase();
|
||||||
|
|
||||||
|
const isInName = filters.name && (filters.exact
|
||||||
|
? searchTerm == name
|
||||||
|
: new RegExp(searchTerm, 'g').test(name));
|
||||||
|
const isInDefinition = filters.definition && (filters.exact
|
||||||
|
? searchTerm == definition
|
||||||
|
: new RegExp(searchTerm, 'g').test(definition));
|
||||||
|
const isInDetails = filters.details && new RegExp(searchTerm, 'g').test(details);
|
||||||
|
return searchTerm === '' || isInName || isInDefinition || isInDetails;
|
||||||
|
});
|
||||||
|
return matchingWords;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.currentDictionary.words
|
||||||
|
}
|
||||||
|
|
||||||
|
export function highlightSearchTerm(word) {
|
||||||
|
let searchTerm = getSearchTerm();
|
||||||
|
if (searchTerm) {
|
||||||
|
const filters = getSearchFilters();
|
||||||
|
const markedUpWord = cloneObject(word);
|
||||||
|
if (filters.ignoreDiacritics) {
|
||||||
|
const searchTermLength = searchTerm.length;
|
||||||
|
searchTerm = removeDiacritics(searchTerm);
|
||||||
|
if (filters.name) {
|
||||||
|
const nameMatches = getIndicesOf(searchTerm, removeDiacritics(markedUpWord.name), filters.caseSensitive);
|
||||||
|
nameMatches.forEach((wordIndex, i) => {
|
||||||
|
wordIndex += '<mark></mark>'.length * i;
|
||||||
|
markedUpWord.name = markedUpWord.name.substring(0, wordIndex)
|
||||||
|
+ '<mark>' + markedUpWord.name.substr(wordIndex, searchTermLength) + '</mark>'
|
||||||
|
+ markedUpWord.name.substr(wordIndex + searchTermLength);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (filters.definition) {
|
||||||
|
const definitionMatches = getIndicesOf(searchTerm, removeDiacritics(markedUpWord.definition), filters.caseSensitive);
|
||||||
|
definitionMatches.forEach((wordIndex, i) => {
|
||||||
|
wordIndex += '<mark></mark>'.length * i;
|
||||||
|
markedUpWord.definition = markedUpWord.definition.substring(0, wordIndex)
|
||||||
|
+ '<mark>' + markedUpWord.definition.substr(wordIndex, searchTermLength) + '</mark>'
|
||||||
|
+ markedUpWord.definition.substr(wordIndex + searchTermLength);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (filters.details) {
|
||||||
|
const detailsMatches = getIndicesOf(searchTerm, removeDiacritics(markedUpWord.details), filters.caseSensitive);
|
||||||
|
detailsMatches.forEach((wordIndex, i) => {
|
||||||
|
wordIndex += '<mark></mark>'.length * i;
|
||||||
|
markedUpWord.details = markedUpWord.details.substring(0, wordIndex)
|
||||||
|
+ '<mark>' + markedUpWord.details.substr(wordIndex, searchTermLength) + '</mark>'
|
||||||
|
+ markedUpWord.details.substr(wordIndex + searchTermLength);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const regexMethod = 'g' + (filters.caseSensitive ? '' : 'i');
|
||||||
|
if (filters.name) {
|
||||||
|
markedUpWord.name = markedUpWord.name.replace(new RegExp(`(${searchTerm})`, regexMethod), `<mark>$1</mark>`);
|
||||||
|
}
|
||||||
|
if (filters.definition) {
|
||||||
|
markedUpWord.definition = markedUpWord.definition.replace(new RegExp(`(${searchTerm})`, regexMethod), `<mark>$1</mark>`);
|
||||||
|
}
|
||||||
|
if (filters.details) {
|
||||||
|
markedUpWord.details = markedUpWord.details.replace(new RegExp(`(${searchTerm})`, regexMethod), `<mark>$1</mark>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return markedUpWord;
|
||||||
|
}
|
||||||
|
return word;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkAllPartsOfSpeechFilters() {
|
||||||
|
const searchFilters = document.querySelectorAll('#searchPartsOfSpeech input[type="checkbox"]');
|
||||||
|
Array.from(searchFilters).forEach(filter => {
|
||||||
|
filter.checked = true;
|
||||||
|
});
|
||||||
|
renderWords();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uncheckAllPartsOfSpeechFilters() {
|
||||||
|
const searchFilters = document.querySelectorAll('#searchPartsOfSpeech input[type="checkbox"]');
|
||||||
|
Array.from(searchFilters).forEach(filter => {
|
||||||
|
filter.checked = false;
|
||||||
|
});
|
||||||
|
renderWords();
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
import {showSection, hideDetailsPanel} from './displayToggles';
|
import {showSection, hideDetailsPanel} from './displayToggles';
|
||||||
import { showSearchModal, clearSearchText, checkAllPartsOfSpeechFilters, uncheckAllPartsOfSpeechFilters } from '../search';
|
import { showSearchModal, clearSearchText, checkAllPartsOfSpeechFilters, uncheckAllPartsOfSpeechFilters } from './search';
|
||||||
import { renderWords, renderInfoModal } from './render';
|
import { renderWords, renderInfoModal } from './render';
|
||||||
|
import helpFile from '../../markdown/help.md';
|
||||||
|
import termsFile from '../../markdown/terms.md';
|
||||||
|
import privacyFile from '../../markdown/privacy.md';
|
||||||
|
|
||||||
export default function setupListeners() {
|
export default function setupListeners() {
|
||||||
setupDetailsTabs();
|
setupDetailsTabs();
|
||||||
|
@ -79,19 +82,13 @@ export function setupSearchFilters() {
|
||||||
|
|
||||||
export function setupInfoButtons() {
|
export function setupInfoButtons() {
|
||||||
document.getElementById('helpInfoButton').addEventListener('click', () => {
|
document.getElementById('helpInfoButton').addEventListener('click', () => {
|
||||||
import('../../markdown/help.md').then(html => {
|
renderInfoModal(helpFile);
|
||||||
renderInfoModal(html);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
document.getElementById('termsInfoButton').addEventListener('click', () => {
|
document.getElementById('termsInfoButton').addEventListener('click', () => {
|
||||||
import('../../markdown/terms.md').then(html => {
|
renderInfoModal(termsFile);
|
||||||
renderInfoModal(html);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
document.getElementById('privacyInfoButton').addEventListener('click', () => {
|
document.getElementById('privacyInfoButton').addEventListener('click', () => {
|
||||||
import('../../markdown/privacy.md').then(html => {
|
renderInfoModal(privacyFile);
|
||||||
renderInfoModal(html);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
export function getWordsStats() {
|
||||||
|
const {words, partsOfSpeech} = window.currentDictionary;
|
||||||
|
const {caseSensitive} = window.currentDictionary.settings;
|
||||||
|
|
||||||
|
const wordStats = {
|
||||||
|
numberOfWords: [
|
||||||
|
{
|
||||||
|
name: 'Total',
|
||||||
|
value: words.length,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
wordLength: {
|
||||||
|
shortest: 0,
|
||||||
|
longest: 0,
|
||||||
|
average: 0,
|
||||||
|
},
|
||||||
|
letterDistribution: [
|
||||||
|
/* {
|
||||||
|
letter: '',
|
||||||
|
number: 0,
|
||||||
|
percentage: 0.00,
|
||||||
|
} */
|
||||||
|
],
|
||||||
|
totalLetters: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
partsOfSpeech.forEach(partOfSpeech => {
|
||||||
|
const wordsWithPartOfSpeech = words.filter(word => word.partOfSpeech === partOfSpeech);
|
||||||
|
wordStats.numberOfWords.push({
|
||||||
|
name: partOfSpeech,
|
||||||
|
value: wordsWithPartOfSpeech.length,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
wordStats.numberOfWords.push({
|
||||||
|
name: 'Unclassified',
|
||||||
|
value: words.filter(word => !partsOfSpeech.includes(word.partOfSpeech)).length,
|
||||||
|
});
|
||||||
|
|
||||||
|
let totalLetters = 0;
|
||||||
|
const numberOfLetters = {};
|
||||||
|
|
||||||
|
words.forEach(word => {
|
||||||
|
const shortestWord = wordStats.wordLength.shortest;
|
||||||
|
const longestWord = wordStats.wordLength.longest;
|
||||||
|
const wordLetters = word.name.split('');
|
||||||
|
const lettersInWord = wordLetters.length;
|
||||||
|
|
||||||
|
totalLetters += lettersInWord;
|
||||||
|
|
||||||
|
if (shortestWord === 0 || lettersInWord < shortestWord) {
|
||||||
|
wordStats.wordLength.shortest = lettersInWord;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (longestWord === 0 || lettersInWord > longestWord) {
|
||||||
|
wordStats.wordLength.longest = lettersInWord;
|
||||||
|
}
|
||||||
|
|
||||||
|
wordLetters.forEach(letter => {
|
||||||
|
const letterToUse = caseSensitive ? letter : letter.toLowerCase();
|
||||||
|
if (!numberOfLetters.hasOwnProperty(letterToUse)) {
|
||||||
|
numberOfLetters[letterToUse] = 1;
|
||||||
|
} else {
|
||||||
|
numberOfLetters[letterToUse]++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
wordStats.totalLetters = totalLetters;
|
||||||
|
wordStats.wordLength.average = words.length > 0 ? Math.round(totalLetters / words.length) : 0;
|
||||||
|
|
||||||
|
for (const letter in numberOfLetters) {
|
||||||
|
if (numberOfLetters.hasOwnProperty(letter)) {
|
||||||
|
const number = numberOfLetters[letter];
|
||||||
|
wordStats.letterDistribution.push({
|
||||||
|
letter,
|
||||||
|
number,
|
||||||
|
percentage: number / totalLetters,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wordStats.letterDistribution.sort((a, b) => {
|
||||||
|
if (a.percentage === b.percentage) return 0;
|
||||||
|
return (a.percentage > b.percentage) ? -1 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return wordStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHomonymnIndexes(word) {
|
||||||
|
const { currentDictionary } = window;
|
||||||
|
const { caseSensitive } = currentDictionary.settings;
|
||||||
|
const foundIndexes = [];
|
||||||
|
currentDictionary.words.forEach((existingWord, index) => {
|
||||||
|
if (existingWord.wordId !== word.wordId
|
||||||
|
&& (caseSensitive ? existingWord.name === word.name : existingWord.name.toLowerCase() === word.name.toLowerCase())) {
|
||||||
|
foundIndexes.push(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return foundIndexes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHomonymnNumber(word) {
|
||||||
|
const homonyms = getHomonymnIndexes(word);
|
||||||
|
if (homonyms.length > 0) {
|
||||||
|
const index = window.currentDictionary.words.findIndex(w => w.wordId === word.wordId);
|
||||||
|
let number = 1;
|
||||||
|
|
||||||
|
for (let i = 0; i < homonyms.length; i++) {
|
||||||
|
if (index < homonyms[i]) break;
|
||||||
|
number++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { wordExists, getHomonymnIndexes } from "./utilities";
|
||||||
|
import removeDiacritics from "../StackOverflow/removeDiacritics";
|
||||||
|
|
||||||
|
export function sortWords() {
|
||||||
|
const { sortByDefinition } = window.currentDictionary.settings;
|
||||||
|
const sortBy = sortByDefinition ? 'definition' : 'name';
|
||||||
|
|
||||||
|
window.currentDictionary.words.sort((wordA, wordB) => {
|
||||||
|
if (removeDiacritics(wordA[sortBy]).toLowerCase() === removeDiacritics(wordB[sortBy]).toLowerCase()) return 0;
|
||||||
|
return removeDiacritics(wordA[sortBy]).toLowerCase() > removeDiacritics(wordB[sortBy]).toLowerCase() ? 1 : -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseReferences(detailsMarkdown) {
|
||||||
|
const references = detailsMarkdown.match(/\{\{.+?\}\}/g);
|
||||||
|
if (references && Array.isArray(references)) {
|
||||||
|
new Set(references).forEach(reference => {
|
||||||
|
let wordToFind = reference.replace(/\{\{|\}\}/g, '');
|
||||||
|
let homonymn = 0;
|
||||||
|
|
||||||
|
if (wordToFind.includes(':')) {
|
||||||
|
const separator = wordToFind.indexOf(':');
|
||||||
|
homonymn = wordToFind.substr(separator + 1);
|
||||||
|
wordToFind = wordToFind.substring(0, separator);
|
||||||
|
if (homonymn && homonymn.trim()
|
||||||
|
&& !isNaN(parseInt(homonymn.trim())) && parseInt(homonymn.trim()) > 0) {
|
||||||
|
homonymn = parseInt(homonymn.trim());
|
||||||
|
} else {
|
||||||
|
homonymn = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let existingWordId = false;
|
||||||
|
const homonymnIndexes = getHomonymnIndexes({ name: wordToFind, wordId: -1 });
|
||||||
|
|
||||||
|
if (homonymn !== false && homonymn > 0) {
|
||||||
|
if (typeof homonymnIndexes[homonymn - 1] !== 'undefined') {
|
||||||
|
existingWordId = window.currentDictionary.words[homonymnIndexes[homonymn - 1]].wordId;
|
||||||
|
}
|
||||||
|
} else if (homonymn !== false) {
|
||||||
|
existingWordId = wordExists(wordToFind, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingWordId !== false) {
|
||||||
|
if (homonymn < 1 && homonymnIndexes.length > 0) {
|
||||||
|
homonymn = 1;
|
||||||
|
}
|
||||||
|
const homonymnSubHTML = homonymn > 0 ? '<sub>' + homonymn.toString() + '</sub>' : '';
|
||||||
|
const wordMarkdownLink = `[${wordToFind}${homonymnSubHTML}](#${existingWordId})`;
|
||||||
|
detailsMarkdown = detailsMarkdown.replace(new RegExp(reference, 'g'), wordMarkdownLink);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return detailsMarkdown;
|
||||||
|
}
|
|
@ -1,12 +1,14 @@
|
||||||
RewriteEngine On # Turn on the rewriting engine
|
RewriteEngine On # Turn on the rewriting engine
|
||||||
|
|
||||||
RewriteRule ^view/([0-9]+)/([0-9]+)/?$ api/router.php?view=word&dict=$1&word=$2 [NC,L] # Handle word ids.
|
RewriteRule ^view/([0-9]+)/([0-9]+)/?$ router.php?view=word&dict=$1&word=$2 [NC,L] # Handle word ids.
|
||||||
|
|
||||||
RewriteRule ^([0-9]+)/([0-9]+)/?$ api/router.php?view=word&dict=$1&word=$2 [NC,L] # Handle word ids.
|
RewriteRule ^([0-9]+)/([0-9]+)/?$ router.php?view=word&dict=$1&word=$2 [NC,L] # Handle word ids.
|
||||||
|
|
||||||
RewriteRule ^view/([0-9]+)/?$ api/router.php?view=dictionary&dict=$1 [NC,L] # Handle dictionary ids.
|
RewriteRule ^view/([0-9]+)/?$ router.php?view=dictionary&dict=$1 [NC,L] # Handle dictionary ids.
|
||||||
|
|
||||||
RewriteRule ^([0-9]+)/?$ api/router.php?view=dictionary&dict=$1 [NC,L] # Handle dictionary ids.
|
RewriteRule ^([0-9]+)/?$ router.php?view=dictionary&dict=$1 [NC,L] # Handle dictionary ids.
|
||||||
|
|
||||||
|
RewriteRule ^/?(index.html)?$ router.php [NC,L] # Handle dictionary ids.
|
||||||
|
|
||||||
#RewriteRule ^issues/?$ https://github.com/Alamantus/Lexiconga/issues [R=301,L] # Shorten issues url.
|
#RewriteRule ^issues/?$ https://github.com/Alamantus/Lexiconga/issues [R=301,L] # Shorten issues url.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"header": "Test",
|
||||||
|
"body": "<p>Test</p>",
|
||||||
|
"expire": "January 1, 2020"
|
||||||
|
}
|
||||||
|
]
|
|
@ -1,12 +1,13 @@
|
||||||
<?php
|
<?php
|
||||||
|
require_once(realpath(dirname(__FILE__) . '/./api/Response.php'));
|
||||||
$view = isset($_GET['view']) ? $_GET['view'] : false;
|
$view = isset($_GET['view']) ? $_GET['view'] : false;
|
||||||
|
|
||||||
switch ($view) {
|
switch ($view) {
|
||||||
case 'dictionary': {
|
case 'dictionary': {
|
||||||
$html = file_get_contents('../template-view.html');
|
$html = file_get_contents(realpath(dirname(__FILE__) . '/./template-view.html'));
|
||||||
$dict = isset($_GET['dict']) ? $_GET['dict'] : false;
|
$dict = isset($_GET['dict']) ? $_GET['dict'] : false;
|
||||||
if ($dict !== false) {
|
if ($dict !== false) {
|
||||||
require_once('./Dictionary.php');
|
require_once(realpath(dirname(__FILE__) . '/./api/Dictionary.php'));
|
||||||
$dictionary = new Dictionary();
|
$dictionary = new Dictionary();
|
||||||
$dictionary_data = $dictionary->getPublicDictionaryDetails($dict);
|
$dictionary_data = $dictionary->getPublicDictionaryDetails($dict);
|
||||||
if ($dictionary_data !== false) {
|
if ($dictionary_data !== false) {
|
||||||
|
@ -22,16 +23,16 @@ switch ($view) {
|
||||||
$html = str_replace('{{public_name}}', 'Error', $html);
|
$html = str_replace('{{public_name}}', 'Error', $html);
|
||||||
$html = str_replace('{{dict_json}}', '{"name": "Error:", "specification": "Dictionary Not Found", "words": []}', $html);
|
$html = str_replace('{{dict_json}}', '{"name": "Error:", "specification": "Dictionary Not Found", "words": []}', $html);
|
||||||
}
|
}
|
||||||
echo $html;
|
return Response::html($html);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'word': {
|
case 'word': {
|
||||||
$html = file_get_contents('../template-view.html');
|
$html = file_get_contents(realpath(dirname(__FILE__) . '/./template-view.html'));
|
||||||
$dict = isset($_GET['dict']) ? $_GET['dict'] : false;
|
$dict = isset($_GET['dict']) ? $_GET['dict'] : false;
|
||||||
$word = isset($_GET['word']) ? $_GET['word'] : false;
|
$word = isset($_GET['word']) ? $_GET['word'] : false;
|
||||||
if ($dict !== false && $word !== false) {
|
if ($dict !== false && $word !== false) {
|
||||||
require_once('./Dictionary.php');
|
require_once(realpath(dirname(__FILE__) . '/./api/Dictionary.php'));
|
||||||
$dictionary = new Dictionary();
|
$dictionary = new Dictionary();
|
||||||
$dictionary_data = $dictionary->getPublicDictionaryDetails($dict);
|
$dictionary_data = $dictionary->getPublicDictionaryDetails($dict);
|
||||||
if ($dictionary_data !== false) {
|
if ($dictionary_data !== false) {
|
||||||
|
@ -61,8 +62,69 @@ switch ($view) {
|
||||||
$html = str_replace('{{public_name}}', 'Error', $html);
|
$html = str_replace('{{public_name}}', 'Error', $html);
|
||||||
$html = str_replace('{{dict_json}}', '{"name": "Error:", "specification": "Dictionary Not Found", "words": []}', $html);
|
$html = str_replace('{{dict_json}}', '{"name": "Error:", "specification": "Dictionary Not Found", "words": []}', $html);
|
||||||
}
|
}
|
||||||
echo $html;
|
return Response::html($html);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
$html = file_get_contents(realpath(dirname(__FILE__) . '/./template-index.html'));
|
||||||
|
$announcements = file_get_contents(realpath(dirname(__FILE__) . '/./announcements.json'));
|
||||||
|
$announcements = json_decode($announcements, true);
|
||||||
|
$announcements_html = '';
|
||||||
|
foreach ($announcements as $announcement) {
|
||||||
|
$expire = strtotime($announcement['expire']);
|
||||||
|
if (time() < $expire) {
|
||||||
|
$announcements_html .= '<article class="announcement">
|
||||||
|
<a class="close-button" title="Close Announcement" onclick="this.parentElement.parentElement.removeChild(this.parentElement);">×︎</a>
|
||||||
|
<h4>' . $announcement['header'] . '</h4>
|
||||||
|
' . $announcement['body'] . '
|
||||||
|
</article>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$html = str_replace('{{announcements}}', $announcements_html, $html);
|
||||||
|
|
||||||
|
$upup_files = array(
|
||||||
|
'src.js',
|
||||||
|
'main.css',
|
||||||
|
'help.html',
|
||||||
|
'privacy.html',
|
||||||
|
'terms.html',
|
||||||
|
'usage.html',
|
||||||
|
'ipa-table.html',
|
||||||
|
'favicon.png',
|
||||||
|
);
|
||||||
|
$files = array_map(function($file_name) use($upup_files) {
|
||||||
|
foreach($upup_files as $index => $upup_file) {
|
||||||
|
$file_pieces = explode('.', $upup_file);
|
||||||
|
if (substr($file_name, 0, strlen($file_pieces[0])) === $file_pieces[0]
|
||||||
|
&& substr($file_name, -strlen($file_pieces[1])) === $file_pieces[1]) {
|
||||||
|
return str_replace('\\', '/', $file_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, scandir('.'));
|
||||||
|
$files = array_filter($files);
|
||||||
|
|
||||||
|
$upup_insert = "<script src=\"upup.min.js\"></script>
|
||||||
|
<script>
|
||||||
|
window.onload = (function (oldLoad) {
|
||||||
|
oldLoad && oldLoad();
|
||||||
|
if (UpUp) {
|
||||||
|
UpUp.start({
|
||||||
|
'cache-version': '2.0.0',
|
||||||
|
'content-url': 'offline.html',
|
||||||
|
'assets': [
|
||||||
|
\"" . implode('","', $files) . "\"
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})(window.onload);
|
||||||
|
</script>";
|
||||||
|
|
||||||
|
$html = str_replace('{{upup_insert}}', $upup_insert, $html);
|
||||||
|
|
||||||
|
return Response::html($html);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -162,6 +162,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.announcement {
|
||||||
|
position: relative;
|
||||||
|
padding: 0 $general-padding $general-padding;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
font-size: 25px;
|
||||||
|
line-height: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
.word {
|
.word {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
@ -182,6 +182,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.announcement {
|
||||||
|
background-color: saturate(lighten($footer-color, 20%), 20%);
|
||||||
|
box-shadow: 4px 4px 5px 0px #000000;
|
||||||
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
background-color: $entry-color;
|
background-color: $entry-color;
|
||||||
border: 1px solid darken($entry-color, 20);
|
border: 1px solid darken($entry-color, 20);
|
||||||
|
|
|
@ -182,6 +182,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.announcement {
|
||||||
|
background-color: saturate(lighten($footer-color, 20%), 20%);
|
||||||
|
box-shadow: 4px 4px 5px 0px $dark;
|
||||||
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
background-color: $entry-color;
|
background-color: $entry-color;
|
||||||
border: 1px solid darken($entry-color, 20);
|
border: 1px solid darken($entry-color, 20);
|
||||||
|
|
|
@ -181,6 +181,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.announcement {
|
||||||
|
background-color: saturate(lighten($footer-color, 20%), 20%);
|
||||||
|
box-shadow: 4px 4px 5px 0px rgba(50, 50, 50, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
background-color: $entry-color;
|
background-color: $entry-color;
|
||||||
border: 1px solid darken($entry-color, 20);
|
border: 1px solid darken($entry-color, 20);
|
||||||
|
|
|
@ -181,6 +181,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.announcement {
|
||||||
|
background-color: saturate(lighten($footer-color, 20%), 20%);
|
||||||
|
box-shadow: 4px 4px 5px 0px rgba(50, 50, 50, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
background-color: $entry-color;
|
background-color: $entry-color;
|
||||||
border: 1px solid darken($entry-color, 20);
|
border: 1px solid darken($entry-color, 20);
|
||||||
|
|
|
@ -182,6 +182,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.announcement {
|
||||||
|
background-color: saturate(lighten($footer-color, 20%), 20%);
|
||||||
|
box-shadow: 4px 4px 5px 0px #000000;
|
||||||
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
background-color: $entry-color;
|
background-color: $entry-color;
|
||||||
border: 1px solid darken($entry-color, 20);
|
border: 1px solid darken($entry-color, 20);
|
||||||
|
|
|
@ -181,6 +181,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.announcement {
|
||||||
|
background-color: saturate(lighten($footer-color, 20%), 20%);
|
||||||
|
box-shadow: 4px 4px 5px 0px rgba(50, 50, 50, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
background-color: $entry-color;
|
background-color: $entry-color;
|
||||||
border: 1px solid darken($entry-color, 20);
|
border: 1px solid darken($entry-color, 20);
|
||||||
|
|
|
@ -181,6 +181,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.announcement {
|
||||||
|
background-color: saturate(lighten($footer-color, 20%), 20%);
|
||||||
|
box-shadow: 4px 4px 5px 0px rgba(50, 50, 50, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
background-color: $entry-color;
|
background-color: $entry-color;
|
||||||
border: 1px solid darken($entry-color, 20);
|
border: 1px solid darken($entry-color, 20);
|
||||||
|
|
|
@ -182,6 +182,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.announcement {
|
||||||
|
background-color: saturate(lighten($footer-color, 20%), 20%);
|
||||||
|
box-shadow: 4px 4px 5px 0px #000000;
|
||||||
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
background-color: $entry-color;
|
background-color: $entry-color;
|
||||||
border: 1px solid darken($entry-color, 20);
|
border: 1px solid darken($entry-color, 20);
|
||||||
|
|
|
@ -182,6 +182,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.announcement {
|
||||||
|
background-color: saturate(lighten($footer-color, 20%), 20%);
|
||||||
|
box-shadow: 4px 4px 5px 0px #000000;
|
||||||
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
background-color: $entry-color;
|
background-color: $entry-color;
|
||||||
border: 1px solid darken($entry-color, 20);
|
border: 1px solid darken($entry-color, 20);
|
||||||
|
|
|
@ -182,6 +182,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.announcement {
|
||||||
|
background-color: saturate(lighten($footer-color, 20%), 20%);
|
||||||
|
box-shadow: 4px 4px 5px 0px #000000;
|
||||||
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
background-color: $entry-color;
|
background-color: $entry-color;
|
||||||
border: 1px solid darken($entry-color, 20);
|
border: 1px solid darken($entry-color, 20);
|
||||||
|
|
|
@ -121,6 +121,7 @@
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section id="mainColumn">
|
<section id="mainColumn">
|
||||||
|
{{announcements}}
|
||||||
<section id="detailsSection">
|
<section id="detailsSection">
|
||||||
<h2 id="dictionaryName">Dictionary Name</h2>
|
<h2 id="dictionaryName">Dictionary Name</h2>
|
||||||
<nav>
|
<nav>
|
||||||
|
@ -369,33 +370,6 @@
|
||||||
|
|
||||||
<div id="messagingSection"></div>
|
<div id="messagingSection"></div>
|
||||||
|
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/UpUp/1.1.0/upup.min.js"></script>
|
{{upup_insert}}
|
||||||
<script>
|
|
||||||
window.onload = (function (oldLoad) {
|
|
||||||
return function () {
|
|
||||||
oldLoad && oldLoad();
|
|
||||||
if (UpUp) {
|
|
||||||
UpUp.start({
|
|
||||||
'cache-version': '2.0.0',
|
|
||||||
'content-url': 'offline.html',
|
|
||||||
'assets': [
|
|
||||||
'src.8d47de22.js',
|
|
||||||
'main.3381ca43.css',
|
|
||||||
'help.ecc721dc.html',
|
|
||||||
'privacy.5cbf7ea0.html',
|
|
||||||
'terms.bcb4cecc.html',
|
|
||||||
'usage.4a51122f.html',
|
|
||||||
'ads.f4e0c99c.js',
|
|
||||||
'favicon.5d013634.png',
|
|
||||||
'ipa-table.6c5cf939.html',
|
|
||||||
'papaparse.min.dab93fe4.js',
|
|
||||||
],
|
|
||||||
'service-worker-url': 'upup.sw.min.js',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})(window.onload);
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
Loading…
Reference in New Issue