Compare commits

...

11 Commits

22 changed files with 409 additions and 289 deletions

3
.gitignore vendored
View File

@ -3,4 +3,5 @@ node_modules/
dist/
vendor/
src/php/api/config.php
src/php/api/config.php
src/php/api/migrate.php

View File

@ -25,3 +25,11 @@ It's less useful, but `npm run serve-frontend-only` will bundle and serve _only_
## 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.
## Migration
There is a script called `src/php/api/migrate.php.changeme` that can be used to help with the migration process from a `version1` Lexiconga database into a `master` database. **Note:** Migration is intended only for migrating from an old server to a freshly-installed/empty new database. To use this, copy `src/php/api/migrate.php.changeme` to `migrate.php` somewhere in the `version1` project (probably in `/php`) and copy the same to `/api/migrate.php` in your `master` project, making sure that all the variables for referencing the databases are correct.
Visit `migrate.php` on your `version1` server with `?outgoing=true` set in order to begin the transfer. The other server's `migrate.php` will receive an "incoming" request multiple times, and your screen will display messages as it works.
_DELETE THESE `migrate.php` FILES IMMEDIATELY AFTER MIGRATION IS COMPLETE!_.

View File

@ -1,16 +1,16 @@
[
{
"header": "Do You Like Lexiconga?",
"body": "If you enjoy Lexiconga, you can help contribute to keeping it online and adding new features. Your donations are much appreciated!",
"cta": "Buy Me a Coffee!",
"link": "https://buymeacoffee.com/robbieantenesse",
"body": "You can contribute a small amount each month toward keeping Lexiconga online and help us keep working on new features. We appreciate whatever you can offer!",
"cta": "Support Lexiconga!",
"link": "https://liberapay.com/robbieantenesse/donate",
"start": "June 1, 2019",
"end": "August 1, 2099",
"isPriority": false
},
{
"header": "Support Lexiconga",
"body": "If you enjoy Lexiconga, you can help contribute to keeping it online and adding new features. Your donations are much appreciated!",
"header": "Support Lexiconga's Developer",
"body": "If you enjoy Lexiconga, you can make a one-time donation to the developer toward keeping it online and adding new features. Your donations are much appreciated!",
"cta": "Buy Me a Coffee!",
"link": "https://buymeacoffee.com/robbieantenesse",
"start": "June 1, 2019",
@ -26,6 +26,15 @@
"end": "March 1, 2020",
"isPriority": false
},
{
"header": "Protect Your Readers",
"body": "RedFlag puts a wall between your readers or followers and content that you want to share. Choose up to 3 different warnings to help them be aware of what they're about to view!",
"cta": "Use RedFlag for Free",
"link": "https://redflag.ga",
"start": "June 1, 2019",
"end": "January 1, 2020",
"isPriority": false
},
{
"header": "Get Theonite: Planet Adyn free!",
"body": "\"This book is the superhero fantasy you didn't know you needed. Magic, Afrofuturism, and heart-pumping suspense - this book has it all!\" - WeReadFantasy.com",

View File

@ -111,7 +111,7 @@
</section>
</main>
<footer id="bottom">
<a href="https://buymeacoff.ee/robbieantenesse" target="_blank" class="small button">Support Lexiconga</a>
<a href="https://liberapay.com/robbieantenesse" target="_blank" class="small button">Support Lexiconga</a>
<a href="https://blog.lexicon.ga" target="_blank" class="small button">Blog</a>
<a href="./" target="_blank" class="small button">Advertise</a>
<a href="https://github.com/Alamantus/Lexiconga/issues" target="_blank" class="small button">Issues</a>

View File

@ -152,7 +152,7 @@
</main>
<footer id="bottom">
<a href="https://buymeacoff.ee/robbieantenesse" target="_blank" class="small button">Support Lexiconga</a>
<a href="https://liberapay.com/robbieantenesse" target="_blank" class="small button">Support Lexiconga</a>
<a href="https://blog.lexicon.ga" target="_blank" class="small button">Blog</a>
<a href="./advertising.html" target="_blank" class="small button">Advertise</a>
<a href="https://github.com/Alamantus/Lexiconga/issues" target="_blank" class="small button">Issues</a>

View File

@ -69,6 +69,15 @@ export function removeTags(html) {
return html;
}
export function shuffle(array) {
// Fisher-Yates shuffle
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
export function slugify(string) {
return removeDiacritics(string).replace(/[^a-zA-Z0-9-_]/g, '-');
}

View File

@ -38,6 +38,7 @@ export function performSync(remoteDictionary) {
if (success) {
renderAll();
document.getElementById('accountSettingsChangeDictionary').value = window.currentDictionary.externalID;
document.getElementById('publicLinkDisplay').style.display = window.currentDictionary.settings.isPublic ? '' : 'none';
} else {
console.error('word sync failed');
}

View File

@ -1,10 +1,10 @@
import { DISPLAY_AD_EVERY } from '../constants.js';
import ads from '../../ads.json';
import { shuffle } from '../helpers.js';
export function setupAds() {
const shuffle = (a, b) => Math.random() > 0.5 ? 1 : -1;
const priority = ads.filter(ad => isActive(ad) && ad.isPriority).sort(shuffle);
const regular = ads.filter(ad => isActive(ad) && !ad.isPriority).sort(shuffle);
const priority = shuffle(ads.filter(ad => isActive(ad) && ad.isPriority));
const regular = shuffle(ads.filter(ad => isActive(ad) && !ad.isPriority));
window.ads = [...priority, ...regular];
}

20
src/js/announcements.js Normal file
View File

@ -0,0 +1,20 @@
import { fadeOutElement } from "./utilities";
import { setCookie, getCookie } from "./StackOverflow/cookie";
export function isDismissed(announcementId) {
let dismissed = getCookie(announcementId);
return dismissed === 'dismissed';
}
export function dismiss(announcement) {
if (announcement.id) {
const expireDate = announcement.dataset.expires;
const now = new Date();
const expire = new Date(expireDate);
const timeDiff = Math.abs(now.getTime() - expire.getTime());
const dayDifference = Math.ceil(timeDiff / (1000 * 3600 * 24));
setCookie(announcement.id, 'dismissed', dayDifference + 1);
}
fadeOutElement(announcement)
}

View File

@ -304,8 +304,11 @@ export function migrateDictionary() {
window.currentDictionary = Object.assign({}, DEFAULT_DICTIONARY, window.currentDictionary);
window.currentDictionary.partsOfSpeech = window.currentDictionary.settings.partsOfSpeech.split(',').map(val => val.trim()).filter(val => val !== '');
delete window.currentDictionary.settings.partsOfSpeech;
delete window.currentDictionary.nextWordId;
window.currentDictionary.settings.sortByDefinition = window.currentDictionary.settings.sortByEquivalent;
delete window.currentDictionary.settings.sortByEquivalent;
window.currentDictionary.settings.theme = 'default';
delete window.currentDictionary.settings.isComplete;
migrated = true;
} else if (window.currentDictionary.version !== MIGRATE_VERSION) {

View File

@ -11,8 +11,11 @@ import { showSearchModal, clearSearchText, checkAllPartsOfSpeechFilters, uncheck
import helpFile from '../markdown/help.md';
import termsFile from '../markdown/terms.md';
import privacyFile from '../markdown/privacy.md';
import { dismiss, isDismissed } from './announcements';
import { fadeOutElement } from './utilities';
export default function setupListeners() {
setupAnnouncements();
setupDetailsTabs();
setupHeaderButtons();
setupWordForm();
@ -34,6 +37,17 @@ export function setupHeaderButtons() {
});
}
function setupAnnouncements() {
const announcements = document.querySelectorAll('.announcement');
Array.from(announcements).forEach(announcement => {
if (announcement.id && isDismissed(announcement.id)) {
fadeOutElement(announcement);
} else {
announcement.querySelector('.close-button').addEventListener('click', () => dismiss(announcement));
}
});
}
function setupDetailsTabs() {
const tabs = document.querySelectorAll('#detailsSection nav li');
tabs.forEach(tab => {

View File

@ -136,31 +136,6 @@ export function getHomonymnNumber(word) {
return 0;
}
export function generateRandomWords(numberOfWords) {
console.log('Generating', numberOfWords, 'words...');
const letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
letters.forEach(letter => letters.push(letter.toUpperCase()));
const words = [];
while (words.length < numberOfWords) {
let word = '';
while (word === '' || words.includes(word)) {
word += letters[Math.floor(Math.random() * letters.length)];
}
words.push(word);
}
words.forEach((word, index) => {
addWord({
name: word,
pronunciation: '/' + word + '/',
partOfSpeech: Math.random() > 0.5 ? 'Noun' : 'Verb',
definition: word,
details: word + (index > 0 ? '\n\nRef: {{' + words[index - 1] + '}}' : ''),
wordId: getNextId(),
}, false);
});
console.log('done');
}
export function addMessage(messageText, time = 5000, extraClass = false) {
const messagingSection = document.getElementById('messagingSection');
const element = document.createElement('div');
@ -174,7 +149,7 @@ export function addMessage(messageText, time = 5000, extraClass = false) {
const closeButton = element.querySelector('.close-button');
const closeMessage = () => {
closeButton.removeEventListener('click', closeMessage);
messagingSection.removeChild(element);
fadeOutElement(element);
};
closeButton.addEventListener('click', closeMessage);
@ -183,6 +158,13 @@ export function addMessage(messageText, time = 5000, extraClass = false) {
}
}
export function fadeOutElement(element) {
element.classList.add('fadeout');
setTimeout(() => {
element.parentElement.removeChild(element);
}, 300);
}
export function hideAllModals() {
const permanentModals = ['#searchModal', '#settingsModal', '#editModal'];
const hideModals = document.querySelectorAll(permanentModals.join(',')),

View File

@ -218,13 +218,13 @@ If you manage to enter your password wrong 10 times, you'll be locked out from l
Please report any problems you come across to the [Lexiconga Issues page](https://github.com/Alamantus/Lexiconga/issues). You can also submit enhancement requests to the same place if you have any requests for new features.
## Update Log
You can see all previous updates to Lexiconga on the [Lexiconga Releases page]](https://github.com/Alamantus/Lexiconga/releases).
You can see all previous updates to Lexiconga on the [Lexiconga Releases page](https://github.com/Alamantus/Lexiconga/releases).
## Open Source
Lexiconga's source code is fully open and readable on Github here: https://github.com/Alamantus/Lexiconga
## Thanks!
If you like Lexiconga and want to buy me a cup of coffee for the service, you can use **[Buy Me A Coffee](https://buymeacoff.ee/robbieantenesse)** to help keep it online if you want.
If you like Lexiconga and want to help contribute to keeping it online and motivate me to keep adding new features, you can use **[Buy Me A Coffee](https://buymeacoff.ee/robbieantenesse)** to give a one-time donation or **[Liberapay](https://liberapay.com/robbieantenesse)** to make a recurring donation.
I hope you enjoy Lexiconga and that it helps you build some awesome languages.

View File

@ -1,7 +1,8 @@
[
{
"header": "Test",
"body": "<p>Test</p>",
"expire": "January 1, 2020"
"header": "Welcome to Lexiconga 2.0!",
"body": "<p>Lexiconga has been rewritten from the ground up!</p><p>Check the <a href=\"https://github.com/Alamantus/Lexiconga/releases\" target=\"_blank\">Updates page</a> for all the new features, or click Help to get a refresher on how to use Lexiconga!</p>",
"expire": "January 1, 2020",
"dismissId": "welcome"
}
]

View File

@ -18,12 +18,12 @@ class User {
if ($user) {
if ($user['old_password'] !== null) {
if ($user['old_password'] === crypt($password, $email)) {
if ($this->upgradePassword($password)) {
if ($this->upgradePassword($password, $user)) {
return $this->logIn($email, $password);
}
}
} else if (password_verify($password, $user['password'])) {
$this->db->execute('UPDATE users SET last_login=' . time() . ' WHERE id=' . $user['id']);
$this->db->execute('UPDATE users SET last_login=current_timestamp() WHERE id=' . $user['id']);
$token = $this->generateUserToken($user['id'], $user['current_dictionary']);
return array(
'token' => $token,
@ -42,7 +42,7 @@ class User {
public function create ($email, $password, $user_data) {
$insert_user_query = 'INSERT INTO users (email, password, public_name, allow_email, created_on)
VALUES (?, ?, ?, ?, ?)';
VALUES (?, ?, ?, ?, current_timestamp())';
$password_hash = password_hash($password, PASSWORD_DEFAULT);
$insert_user = $this->db->execute($insert_user_query, array(
@ -50,7 +50,6 @@ VALUES (?, ?, ?, ?, ?)';
$password_hash,
$user_data['publicName'] !== '' ? $user_data['publicName'] : null,
$user_data['allowEmail'] ? 1 : 0,
time(),
));
if ($insert_user === true) {
$new_user_id = $this->db->lastInsertId();
@ -346,11 +345,11 @@ VALUES (?, ?, ?, ?, ?)';
}
private function hasMembership ($id) {
$current_membership = "SELECT * FROM memberships WHERE user=$id AND start_date>=CURRENT_TIMESTAMP AND CURRENT_TIMESTAMP<expire_date";
$current_membership = "SELECT * FROM memberships WHERE user=$id AND start_date>=current_timestamp() AND current_timestamp()<expire_date";
return $this->db->query($current_membership)->rowCount() > 0;
}
private function upgradePassword ($password) {
private function upgradePassword($password, $user) {
$new_password = password_hash($password, PASSWORD_DEFAULT);
$update_query = 'UPDATE users SET old_password=NULL, password=? WHERE id=' . $user['id'];
$stmt = $this->db->query($update_query, array($new_password));

View File

@ -0,0 +1,274 @@
<?php
$incoming = isset($_POST['incoming']) ? json_decode($_POST['incoming'], true) : false;
$outgoing = isset($_GET['outgoing']);
header('Access-Control-Allow-Origin: *');
header('Expires: Sun, 01 Nov 2015 22:46:51 GMT');
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
define("VERSION1_DATABASE_SERVERNAME", "host");
define("VERSION1_DATABASE_USERNAME", "username");
define("VERSION1_DATABASE_PASSWORD", "password");
define("VERSION1_DATABASE_NAME", "database");
define("MIGRATE_TO", "remote_url");
define("NEW_DATABASE_SERVERNAME", "host");
define("NEW_DATABASE_USERNAME", "username");
define("NEW_DATABASE_PASSWORD", "password");
define("NEW_DATABASE_NAME", "database");
if ($outgoing) {
echo '<pre>' . PHP_EOL;
echo 'Starting migration from ' . VERSION1_DATABASE_SERVERNAME . ' db ' . VERSION1_DATABASE_NAME . PHP_EOL . 'To ' . MIGRATE_TO . PHP_EOL . PHP_EOL;
$dbconnection = new PDO('mysql:host=' . VERSION1_DATABASE_SERVERNAME . ';dbname=' . VERSION1_DATABASE_NAME . ';charset=utf8', VERSION1_DATABASE_USERNAME, VERSION1_DATABASE_PASSWORD);
$dbconnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// $dbconnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
$dbconnection->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
try {
$users_stmt = $dbconnection->prepare('SELECT * FROM `users`');
$users_stmt->execute();
if ($users_stmt) {
$users = $users_stmt->fetchAll();
if ($users !== false) {
echo count($users) . ' Users' . PHP_EOL;
foreach($users as $user) {
$send_user_response = send('user', $user);
if ($send_user_response === "GOT USER") {
echo PHP_EOL . 'User ' . $user['id'] . ' sent successfully' . PHP_EOL;
$dictionaries_stmt = $dbconnection->prepare('SELECT * FROM `dictionaries` WHERE `user`=?');
$dictionaries_stmt->execute(array(intval($user['id'])));
if ($dictionaries_stmt) {
$dictionaries = $dictionaries_stmt->fetchAll();
if ($dictionaries !== false) {
echo 'User ' . $user['id'] . ' has ' . count($dictionaries) . ' Dictionaries' . PHP_EOL;
$send_dictionaries_response = send('dictionaries', $dictionaries, array('user' => $user['id']));
if ($send_dictionaries_response === 'GOT DICTIONARIES') {
echo 'Dictionaries sent successfully' . PHP_EOL;
foreach($dictionaries as $dictionary) {
$words_stmt = $dbconnection->prepare('SELECT * FROM `words` WHERE `dictionary`=?');
$words_stmt->execute(array(intval($dictionary['id'])));
if ($words_stmt) {
$words = $words_stmt->fetchAll();
if ($words !== false) {
$send_words_response = send('words', $words);
if ($send_words_response === 'GOT WORDS') {
echo 'Words for Dictionary ' . $dictionary['id'] . ' sent successfully' . PHP_EOL;
} else {
echo var_export($send_words_response, true) . PHP_EOL . PHP_EOL;
}
} else {
echo '$words_stmt->fetchAll() failed.' . PHP_EOL . var_export($words_stmt->errorInfo(), true) . PHP_EOL . PHP_EOL;
}
} else {
echo '$words_stmt failed' . PHP_EOL . PHP_EOL;
}
}
} else {
echo var_export($send_dictionaries_response, true) . PHP_EOL . PHP_EOL;
}
} else {
echo '$dictionaries_stmt->fetchAll() failed.' . PHP_EOL . var_export($dictionaries_stmt->errorInfo(), true) . PHP_EOL . PHP_EOL;
}
} else {
echo '$dictionaries_stmt failed' . PHP_EOL . PHP_EOL;
}
} else {
var_dump($send_user_response) . PHP_EOL . PHP_EOL;
}
}
} else {
echo '$users_stmt->fetchAll() failed.' . PHP_EOL . var_export($users_stmt->errorInfo(), true) . PHP_EOL . PHP_EOL;
}
} else {
echo '$users_stmt failed' . PHP_EOL . PHP_EOL;
}
}
catch (PDOException $ex) {
echo var_export($ex, true) . PHP_EOL . PHP_EOL;
}
echo '</pre>';
} else if ($incoming !== false) {
$type = isset($_POST['type']) ? $_POST['type'] : false;
if ($type !== false) {
$dbconnection = new PDO('mysql:host=' . NEW_DATABASE_SERVERNAME . ';dbname=' . NEW_DATABASE_NAME . ';charset=utf8', NEW_DATABASE_USERNAME, NEW_DATABASE_PASSWORD);
$dbconnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
switch($type) {
case 'user': {
if ($incoming === null) {
echo 'Invalid JSON';
} else {
$user_stmt = $dbconnection->prepare('INSERT INTO `users`(`id`, `email`, `old_password`, `password`, `public_name`, `current_dictionary`, `allow_email`, `last_login`, `password_reset_code`, `password_reset_date`, `created_on`) VALUES (?,?,?,?,?,?,?,?,?,?,?)');
$user_stmt->execute(array(
intval($incoming['id']),
$incoming['email'],
$incoming['password'],
null,
$incoming['public_name'],
intval($incoming['current_dictionary']),
intval($incoming['allow_email']),
$incoming['last_login'],
null,
null,
$incoming['created_on'],
));
if ($user_stmt) {
echo 'GOT USER';
} else {
var_dump($user_stmt->errorInfo());
}
}
break;
}
case 'dictionaries': {
if ($incoming === null) {
echo 'Invalid JSON';
} else {
if (count($incoming) === 0) {
echo 'Need new dictionary for user ' . $_POST['user'] . ': ';
$new_id = mt_rand(1000, 999999999);
$user = intval($_POST['user']);
$insert_dictionary_query = "INSERT INTO dictionaries (id, user, description, created_on) VALUES (?, ?, ?, ?)";
$insert_dictionary_stmt = $dbconnection->prepare($insert_dictionary_query);
$insert_dictionary = $insert_dictionary_stmt->execute(array($new_id, $user, 'A new dictionary.', time()));
if ($insert_dictionary) {
$insert_linguistics_query = "INSERT INTO dictionary_linguistics (dictionary, parts_of_speech, exceptions, orthography_notes, grammar_notes) VALUES ($new_id, ?, ?, ?, ?)";
$insert_linguistics_stmt = $dbconnection->prepare($insert_linguistics_query);
$insert_linguistics = $insert_linguistics_stmt->execute(array('Noun,Adjective,Verb', '', '', ''));
if ($insert_linguistics === true) {
$update_query = 'UPDATE users SET current_dictionary=? WHERE id=?';
$update_stmt = $dbconnection->prepare($update_query);
$update = $update_stmt->execute(array($new_id, $user));
if ($update) {
echo 'CREATED DICTIONARY';
} else {
var_dump($update_stmt);
}
} else {
var_dump($insert_linguistics_stmt->errorInfo());
}
} else {
var_dump($insert_dictionary_stmt->errorInfo());
}
} else {
$dictionaries_query = 'INSERT INTO `dictionaries`(`id`, `user`, `name`, `specification`, `description`, `allow_duplicates`, `case_sensitive`, `sort_by_definition`, `theme`, `is_public`, `last_updated`, `created_on`) VALUES ';
$linguistics_query = "INSERT INTO dictionary_linguistics (dictionary, parts_of_speech, exceptions, orthography_notes, grammar_notes) VALUES ";
$dictionaries_params = array();
$linguistics_params = array();
foreach($incoming as $dictionary) {
$dictionaries_query .= '(?,?,?,?,?,?,?,?,?,?,?,?), ';
// `id`, `user`, `name`, `description`, `words`, `next_word_id`, `allow_duplicates`, `case_sensitive`, `parts_of_speech`, `sort_by_equivalent`, `is_complete`, `is_public`, `last_updated`, `created_on`
$dictionaries_params[] = intval($dictionary['id']);
$dictionaries_params[] = intval($dictionary['user']);
$dictionaries_params[] = $dictionary['name'];
$dictionaries_params[] = 'Dictionary';
$dictionaries_params[] = fixStupidOldNonsense($dictionary['description']);
$dictionaries_params[] = intval($dictionary['allow_duplicates']);
$dictionaries_params[] = intval($dictionary['case_sensitive']);
$dictionaries_params[] = intval($dictionary['sort_by_equivalent']);
$dictionaries_params[] = 'default';
$dictionaries_params[] = intval($dictionary['is_public']);
$dictionaries_params[] = $dictionary['last_updated'] ? strtotime($dictionary['last_updated']) : null;
$dictionaries_params[] = strtotime($dictionary['created_on']);
$linguistics_query .= '(?, ?, ?, ?, ?), ';
$linguistics_params[] = intval($dictionary['id']);
$linguistics_params[] = $dictionary['parts_of_speech'];
$linguistics_params[] = '';
$linguistics_params[] = '';
$linguistics_params[] = '';
}
$dictionaries_stmt = $dbconnection->prepare(trim($dictionaries_query, ', '));
$dictionaries_stmt->execute($dictionaries_params);
if ($dictionaries_stmt) {
$linguistics_stmt = $dbconnection->prepare(trim($linguistics_query, ', '));
$linguistics_stmt->execute($linguistics_params);
if ($linguistics_stmt) {
echo 'GOT DICTIONARIES';
} else {
var_dump($linguistics_stmt->errorInfo());
}
} else {
var_dump($dictionaries_stmt->errorInfo());
}
}
}
break;
}
case 'words': {
if ($incoming === null) {
echo 'Invalid JSON';
} else {
if (count($incoming) > 0) {
$words_query = 'INSERT INTO `words`(`dictionary`, `word_id`, `name`, `pronunciation`, `part_of_speech`, `definition`, `details`, `last_updated`, `created_on`) VALUES ';
$words_params = array();
foreach($incoming as $word) {
$words_query .= '(?,?,?,?,?,?,?,?,?), ';
// `dictionary`, `word_id`, `name`, `pronunciation`, `part_of_speech`, `simple_definition`, `long_definition`, `last_updated`, `created_on`
$words_params[] = intval($word['dictionary']);
$words_params[] = intval($word['word_id']);
$words_params[] = $word['name'];
$words_params[] = $word['pronunciation'];
$words_params[] = $word['part_of_speech'];
$words_params[] = $word['simple_definition'];
$words_params[] = fixStupidOldNonsense($word['long_definition']);
$words_params[] = $word['last_updated'] ? strtotime($word['last_updated']) : null;
$words_params[] = strtotime($word['created_on']);
}
$words_stmt = $dbconnection->prepare(trim($words_query, ', '));
$words_stmt->execute($words_params);
if ($words_stmt) {
echo 'GOT WORDS';
} else {
var_dump($words_stmt->errorInfo());
}
} else {
echo 'NO WORDS';
}
}
break;
}
}
}
} else {
echo 'no' . PHP_EOL . PHP_EOL;
}
function send($type, $data, $additional_data = array()) {
$send_data = array_merge(array('type' => $type, 'incoming' => json_encode($data)), $additional_data);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, MIGRATE_TO . '/api/migrate.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($send_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$server_output = curl_exec($ch);
curl_close ($ch);
return $server_output;
}
function fixStupidOldNonsense($markdown) {
$markdown = str_replace('&quot;', '"', $markdown);
$markdown = str_replace('&apos;', "'", $markdown);
$markdown = str_replace('&#92;', '\\', $markdown);
$markdown = str_replace('<br>', '\\n', $markdown);
return $markdown;
}

View File

@ -75,8 +75,8 @@ switch ($view) {
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>
$announcements_html .= '<article class="announcement"' . (isset($announcement['dismissId']) ? ' id="announcement-' . $announcement['dismissId'] . '"' : '') . ' data-expires="' . $announcement['expire'] . '">
<a class="close-button" title="Dismiss Announcement">&times;&#xFE0E;</a>
<h4>' . $announcement['header'] . '</h4>
' . $announcement['body'] . '
</article>';

View File

@ -212,6 +212,25 @@ span .tag {
}
}
.fadeout {
overflow: hidden;
animation-name: shut;
animation-duration: 0.3s;
animation-timing-function: linear;
animation-iteration-count: 1;
animation-fill-mode: both;
}
@keyframes shut {
0% {
opacity: 1;
max-height: 200px;
}
100% {
opacity: 0;
max-height: 0px;
}
}
.pagination {
position: relative;
text-align: center;

View File

@ -21,7 +21,7 @@ CREATE TABLE IF NOT EXISTS `dictionaries` (
`last_updated` int(11) DEFAULT NULL,
`created_on` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=500 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
) ENGINE=MyISAM AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
DELIMITER $$
CREATE TRIGGER IF NOT EXISTS `delete_dictionary_parts` AFTER DELETE ON `dictionaries` FOR EACH ROW BEGIN
DELETE FROM words WHERE words.dictionary=old.id;
@ -33,16 +33,16 @@ DELIMITER ;
CREATE TABLE IF NOT EXISTS `dictionary_linguistics` (
`dictionary` int(11) NOT NULL,
`parts_of_speech` text COLLATE utf8_unicode_ci NOT NULL COMMENT 'Comma-separated',
`consonants` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'Space-separated',
`vowels` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'Space-separated',
`blends` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'Space-separated',
`onset` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'Comma-separated',
`nucleus` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'Comma-separated',
`coda` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'Comma-separated',
`exceptions` text COLLATE utf8_unicode_ci NOT NULL COMMENT 'Markdown',
`orthography_notes` text COLLATE utf8_unicode_ci NOT NULL COMMENT 'Markdown',
`grammar_notes` text COLLATE utf8_unicode_ci NOT NULL COMMENT 'Markdown',
`parts_of_speech` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT 'Comma-separated',
`consonants` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'Space-separated',
`vowels` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'Space-separated',
`blends` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'Space-separated',
`onset` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'Comma-separated',
`nucleus` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'Comma-separated',
`coda` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'Comma-separated',
`exceptions` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT 'Markdown',
`orthography_notes` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT 'Markdown',
`grammar_notes` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT 'Markdown',
UNIQUE KEY `dictionary` (`dictionary`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@ -62,13 +62,13 @@ CREATE TABLE IF NOT EXISTS `users` (
`public_name` varchar(50) COLLATE utf8_unicode_ci NOT NULL DEFAULT 'Someone',
`current_dictionary` int(11) DEFAULT NULL,
`allow_email` tinyint(1) NOT NULL DEFAULT 1,
`last_login` int(11) DEFAULT NULL,
`last_login` datetime DEFAULT NULL,
`password_reset_code` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL,
`password_reset_date` datetime DEFAULT NULL,
`created_on` int(11) NOT NULL,
`password_reset_date` timestamp NULL DEFAULT NULL,
`created_on` datetime DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
) ENGINE=MyISAM AUTO_INCREMENT=200 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
) ENGINE=MyISAM AUTO_INCREMENT=500 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
DELIMITER $$
CREATE TRIGGER IF NOT EXISTS `Delete_User_Dictionaries` AFTER DELETE ON `users` FOR EACH ROW DELETE FROM dictionaries WHERE dictionaries.user = old.id
$$

View File

@ -152,7 +152,7 @@
</main>
<footer id="bottom">
<a href="https://buymeacoff.ee/robbieantenesse" target="_blank" class="small button">Support Lexiconga</a>
<a href="https://liberapay.com/robbieantenesse" target="_blank" class="small button">Support Lexiconga</a>
<a href="https://blog.lexicon.ga" target="_blank" class="small button">Blog</a>
<a href="./advertising.html" target="_blank" class="small button">Advertise</a>
<a href="https://github.com/Alamantus/Lexiconga/issues" target="_blank" class="small button">Issues</a>

View File

@ -5,11 +5,6 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Password Reset | Lexiconga</title>
<meta property="og:url" content="https://lexicon.ga/advertising.html" />
<meta property="og:type" content="article" />
<meta property="og:title" content="Advertising on Lexiconga" />
<meta property="og:description" content="Buy advertisement space on Lexiconga" />
<!--meta property="og:image" content="http://lexicon.ga/images/logo.svg" /-->
<link rel="icon" href="src/images/favicon.png" type="image/x-icon" />
<link rel="stylesheet" href="src/main.scss" />
<script src="src/js/account/passwordReset.js"></script>
@ -46,7 +41,7 @@
</section>
</main>
<footer id="bottom">
<a href="https://buymeacoff.ee/robbieantenesse" target="_blank" class="small button">Support Lexiconga</a>
<a href="https://liberapay.com/robbieantenesse" target="_blank" class="small button">Support Lexiconga</a>
<a href="https://blog.lexicon.ga" target="_blank" class="small button">Blog</a>
<a href="./" target="_blank" class="small button">Advertise</a>
<a href="https://github.com/Alamantus/Lexiconga/issues" target="_blank" class="small button">Issues</a>

View File

@ -101,38 +101,10 @@
</div>
</section>
<!-- div id="headerMenu">
<a id="settingsButton" class="button">Settings</a>
<a id="loginCreateAccountButton" class="button">Log In&nbsp;/ Create Account</a>
</div -->
<div style="clear:both;"></div>
</header>
<main>
<!--aside id="sideColumn">
<div id="mobileWordFormShow">+</div>
<form id="wordForm">
<label>Word<span class="red">*</span><br>
<input id="wordName" maxlength="200">
</label>
<label>Pronunciation<a class="label-button ipa-table-button">IPA Chart</a><br>
<input id="wordPronunciation" class="ipa-field" maxlength="200"><br>
<a class="label-help-button ipa-field-help-button">Field Help</a>
</label>
<label>Part of Speech<br>
<select id="wordPartOfSpeech" class="part-of-speech-select"></select>
</label>
<label>Definition<span class="red">*</span><br>
<input id="wordDefinition" maxlength="2500" placeholder="Equivalent words">
</label>
<label>Details<span class="red">*</span><a class="label-button maximize-button">Maximize</a><br>
<textarea id="wordDetails" placeholder="Markdown formatting allowed"></textarea>
</label>
<div id="wordErrorMessage"></div>
<a class="button" id="addWordButton">Add Word</a>
</form>
</aside -->
<section id="mainColumn">
<section id="detailsSection">
<a id="dictionaryShare" href="./" class="button" title="Link to Dictionary" style="float:right;">&#10150;</a>
@ -140,7 +112,7 @@
<h4>Created by {{public_name}}</h4>
<nav>
<ul>
<li>Description</li><li>Details</li><li>Stats</li><!-- li id="editDictionaryButton">Edit</li -->
<li>Description</li><li>Details</li><li>Stats</li>
</ul>
</nav>
<article id="detailsPanel" style="display:none;">
@ -166,7 +138,7 @@
</main>
<footer id="bottom">
<a href="https://buymeacoff.ee/robbieantenesse" target="_blank" class="small button">Support Lexiconga</a>
<a href="https://liberapay.com/robbieantenesse" target="_blank" class="small button">Support Lexiconga</a>
<a href="https://blog.lexicon.ga" target="_blank" class="small button">Blog</a>
<a href="./advertising.html" target="_blank" class="small button">Advertise</a>
<a href="https://github.com/Alamantus/Lexiconga/issues" target="_blank" class="small button">Issues</a>
@ -177,193 +149,6 @@
<a class="button" id="privacyInfoButton">Privacy</a>
</footer>
<!-- section id="settingsModal" class="modal" style="display:none;">
<div class="modal-background" onclick="this.parentElement.style.display='none';"></div>
<div class="modal-content">
<a class="close-button" onclick="this.parentElement.parentElement.style.display='none';">&times;&#xFE0E;</a>
<section>
<form class="split two">
<div>
<h3>General Settings</h3>
<label>Use IPA Auto-Fill
<input id="settingsUseIPA" type="checkbox" checked><br />
<small>Check this to use character combinations to input International Phonetic Alphabet characters into
Pronunciation fields.</small>
</label>
<label>Use Hotkeys
<input id="settingsUseHotkeys" type="checkbox" checked><br />
<small>Check this to enable keyboard combinations to perform different helpful actions.</small>
</label>
<label>Theme
<select disabled>
<option selected value="default">Default</option>
<option value="dark">Dark</option>
<option value="light">Light</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
<option value="royal">Royal</option>
</select>
</label>
<div id="accountSettings"></div>
</div>
<div id="accountActions"></div>
</form>
</section>
<footer>
<a class="button" id="settingsSave">Save</a>
<a class="button" id="settingsSaveAndClose">Save &amp; Close</a>
<a class="red button" onclick="this.parentElement.parentElement.parentElement.style.display='none';">Close Without Saving</a>
</footer>
</div>
</section -->
<!-- section id="editModal" class="modal" style="display:none;">
<div class="modal-background" onclick="this.parentElement.style.display='none';"></div>
<div class="modal-content">
<a class="close-button" onclick="this.parentElement.parentElement.style.display='none';">&times;&#xFE0E;</a>
<nav class="tabs">
<ul>
<li class="active">Description</li><li>Details</li><li>Settings</li><li>Actions</li>
</ul>
</nav>
<section id="editDescriptionTab">
<label>Name<br>
<input id="editName" maxlength="50">
</label>
<label>Specification<br>
<input id="editSpecification" maxlength="50">
</label>
<label>Description<a class="label-button maximize-button">Maximize</a><br>
<textarea id="editDescription"></textarea>
</label>
</section>
<section id="editDetailsTab" style="display:none;">
<label>Parts of Speech <small>(Comma Separated List)</small><br>
<input id="editPartsOfSpeech" maxlength="2500" placeholder="Noun,Adjective,Verb">
</label>
<label>Alphabetical Order <small>(Comma Separated List. Include every letter!)</small><br>
<input id="editAlphabeticalOrder" disabled value="English Alphabet">
</label>
<h3>Phonology</h3>
<div class="split three">
<div>
<label>Consonants<br>
<small>(Space separated list)</small><br>
<input id="editConsonants" class="ipa-field" maxlength="100" placeholder="p b m n t ..."><br>
<a class="label-help-button ipa-field-help-button">Field Help</a>
<a class="label-button ipa-table-button">IPA Chart</a>
</label>
</div>
<div>
<label>Vowels<br>
<small>(Space separated list)</small><br>
<input id="editVowels" class="ipa-field" maxlength="100" placeholder="æ u e ɪ ..."><br>
<a class="label-help-button ipa-field-help-button">Field Help</a>
<a class="label-button ipa-table-button">IPA Chart</a>
</label>
</div>
<div>
<label>Polyphthongs&nbsp;/ Blends<br>
<small>(Space separated list)</small><br>
<input id="editBlends" class="ipa-field" maxlength="100" placeholder="ai ou ue ..."><br>
<a class="label-help-button ipa-field-help-button">Field Help</a>
<a class="label-button ipa-table-button">IPA Chart</a>
</label>
</div>
</div>
<h3>Phonotactics</h3>
<div class="split three">
<div>
<label>Onset<br>
<small>(Comma separated list)</small><br>
<input id="editOnset" maxlength="100" placeholder="Consonants,Vowels">
</label>
</div>
<div>
<label>Nucleus<br>
<small>(Comma separated list)</small><br>
<input id="editNucleus" maxlength="100" placeholder="Vowels,Blends">
</label>
</div>
<div>
<label>Coda<br>
<small>(Comma separated list)</small><br>
<input id="editCoda" maxlength="100" placeholder="Any">
</label>
</div>
</div>
<label>Exceptions <small>(Markdown-enabled)</small><br>
<textarea id="editExceptions"></textarea>
</label>
<h3>Orthography</h3>
<label>Notes <small>(Markdown-enabled)</small><a class="label-button maximize-button">Maximize</a><br>
<textarea id="editOrthography"></textarea>
</label>
<h3>Grammar</h3>
<label>Notes <small>(Markdown-enabled)</small><a class="label-button maximize-button">Maximize</a><br>
<textarea id="editGrammar"></textarea>
</label>
</section>
<section id="editSettingsTab" style="display:none;">
<label>Prevent Duplicate Words
<input type="checkbox" id="editPreventDuplicates"><br>
<small>Checking this box will prevent the creation of words with the exact same spelling.</small>
</label>
<label>Words are Case-Sensitive
<input type="checkbox" id="editCaseSensitive"><br>
<small>Checking this box will allow the creation of words with the exact same spelling if their capitalization is different.</small>
</label>
<label>Sort by Definition
<input type="checkbox" id="editSortByDefinition"><br>
<small>Checking this box will sort the words in alphabetical order based on the Definition instead of the Word.</small>
</label>
</section>
<section id="editActionsTab" style="display:none;">
<h3>Import&nbsp;/ Export</h3>
<div class="split two">
<div>
<p>
<label class="button">Import JSON <input type="file" id="importDictionaryFile" accept="application/json, .dict"><br>
<small>Import a previously-exported <code>JSON</code> file.</small>
</label>
</p>
<p>
<label class="button">Import Words <input type="file" id="importWordsCSV" accept="text/csv, .csv"><br>
<small>Import a CSV file of words.</small>
</label>
<a class="small button" download="Lexiconga_import-template.csv" href="data:text/csv;charset=utf-8,%22word%22,%22pronunciation%22,%22part of speech%22,%22definition%22,%22explanation%22%0A">Download an example file with the correct formatting</a>
</p>
</div>
<div>
<p>
<a class="button" id="exportDictionaryButton">Export JSON</a><br>
<small>Export your work as a <code>JSON</code> file to re-import later.</small>
</p>
<p>
<a class="button" id="exportWordsButton">Export Words</a><br>
<small>Export a CSV file of your words.</small>
</p>
</div>
</div>
<p>
<a class="red button" id="deleteDictionaryButton">Delete Dictionary</a><br>
<small>This will permanently delete your current dictionary, and it will not be possible to return it if you have not backed it up!</small>
</p>
</section>
<footer>
<a class="button" id="editSave">Save</a>
<a class="button" id="editSaveAndClose">Save &amp; Close</a>
<a class="red button" onclick="this.parentElement.parentElement.parentElement.style.display='none';">Close Without Saving</a>
</footer>
</div>
</section -->
<div id="messagingSection"></div>
</body>
</html>