Compare commits

...

11 Commits

Author SHA1 Message Date
Robbie Antenesse ab75db9a39 Don't upload dictionaries have not been edited 2019-06-06 16:06:17 -06:00
Robbie Antenesse 428af8897e Put homonymn number on public dictionary entries 2019-06-06 15:41:30 -06:00
Robbie Antenesse 03f65ec6b6 Automate UpUp asset reference via router.php 2019-06-06 15:08:00 -06:00
Robbie Antenesse e431dacd1d Style announcements 2019-06-06 13:17:41 -06:00
Robbie Antenesse a881adc667 Move index to template-index.html; Use router to render index
Makes it possible to render announcements more easily
2019-06-06 13:17:13 -06:00
Robbie Antenesse 140f3c5c8f Move router.php to root; use realpath for file refs 2019-06-06 13:14:46 -06:00
Robbie Antenesse 07703f7628 Update UpUp asset 2019-06-06 11:51:20 -06:00
Robbie Antenesse 4134976cd7 Sort words in public view correctly 2019-06-06 11:50:26 -06:00
Robbie Antenesse 4bf0c9c692 Always show IPA table button in pronunciation fields 2019-06-06 11:36:27 -06:00
Robbie Antenesse b4d9635d15 Only split out html/md files;Fix page load problem 2019-06-06 11:25:17 -06:00
Robbie Antenesse 11167a639b Shrink view js by splitting out only what is needed 2019-06-06 11:03:00 -06:00
34 changed files with 626 additions and 231 deletions

View File

@ -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.

View File

@ -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/*"

View File

@ -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);

View File

@ -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);

View File

@ -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...');

View File

@ -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 => {

View File

@ -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) {

View File

@ -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);
} }

View File

@ -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() {

View File

@ -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">&times;&#xFE0E;</a>
<a class="close-button">&times;&#xFE0E;</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) {

View File

@ -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 => {

View File

@ -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);
});
}); });
} }

View File

@ -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);
}

View File

@ -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);

View File

@ -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">&#10150;</a> <a href="${shareLink}" target="_blank" class="small button word-option-button" title="Link to Word">&#10150;</a>

145
src/js/view/search.js Normal file
View File

@ -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();
}

View File

@ -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);
});
}); });
} }

118
src/js/view/utilities.js Normal file
View File

@ -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;
}

View File

@ -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;
}

View File

@ -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.

View File

@ -0,0 +1,7 @@
[
{
"header": "Test",
"body": "<p>Test</p>",
"expire": "January 1, 2020"
}
]

View File

@ -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);">&times;&#xFE0E;</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;
}
} }

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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>