v3 init push
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace Franzz\MyThoughts;
|
||||
|
||||
use Franzz\Objects\Db;
|
||||
use Franzz\Objects\Main;
|
||||
use Franzz\Objects\Translator;
|
||||
use Settings;
|
||||
|
||||
/* Timezones
|
||||
* ---------
|
||||
* Every request carries the browser's IANA timezone in `t`, which Main applies
|
||||
* to PHP and to the MySQL session. Timestamps therefore go in and come out in
|
||||
* the reader's own time. On top of that each entry stores the timezone it was
|
||||
* written in, so a journal written across a move or a trip still shows each
|
||||
* page stamped with the local time of the moment it was written - and the
|
||||
* client formats from a UNIX timestamp, never from a preformatted string.
|
||||
*/
|
||||
class MyThoughts extends Main {
|
||||
public const PROJECT_NAME = 'MyThoughts';
|
||||
public const DEFAULT_LANG = 'en';
|
||||
|
||||
//The dictionaries shipped in resources/lang. Listed rather than globbed:
|
||||
//Translator resolves its folder relative to the calling script, and the
|
||||
//settings panel needs the list on a page load either way.
|
||||
public const LANGUAGES = ['en', 'fr'];
|
||||
|
||||
/* "Never happened", for the TIMESTAMP columns declared DEFAULT 0.
|
||||
*
|
||||
* Not date(TIMESTAMP_FORMAT, 0): that formats the epoch in the session's
|
||||
* timezone, which east of Greenwich lands before 1970-01-01 00:00:01 UTC -
|
||||
* below what a MySQL TIMESTAMP can hold - and is rejected outright under
|
||||
* STRICT_TRANS_TABLES. This is the same literal the columns default to. */
|
||||
public const ZERO_TIMESTAMP = '0000-00-00 00:00:00';
|
||||
|
||||
private const MAIN_PAGE = 'index';
|
||||
private const VITE_APP = 'src/app.js';
|
||||
|
||||
private User $oUser;
|
||||
private Journal $oJournal;
|
||||
|
||||
public function __construct($sProcessPage, $sTimezone) {
|
||||
parent::__construct($sProcessPage, true, $sTimezone);
|
||||
|
||||
$this->oUser = new User($this->oDb);
|
||||
$this->oLang = new Translator($this->oUser->getLang(), self::DEFAULT_LANG);
|
||||
$this->oJournal = new Journal($this->oDb, $this->oUser);
|
||||
}
|
||||
|
||||
public function getUser(): User {
|
||||
return $this->oUser;
|
||||
}
|
||||
|
||||
public function getJournal(): Journal {
|
||||
return $this->oJournal;
|
||||
}
|
||||
|
||||
protected function install() {
|
||||
$this->oDb->install();
|
||||
}
|
||||
|
||||
protected function getSqlOptions() {
|
||||
return [
|
||||
'tables' => [
|
||||
User::USER_TABLE => ['name', 'email', 'password', 'token', 'token_exp', 'language', 'timezone', 'clearance'],
|
||||
Journal::ENTRY_TABLE=> [Db::getId(User::USER_TABLE), 'content', 'status', 'started_on', 'closed_on', 'timezone']
|
||||
],
|
||||
'types' => [
|
||||
'name' => 'VARCHAR(100) NOT NULL',
|
||||
'email' => 'VARCHAR(320) NOT NULL',
|
||||
'password' => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||
'token' => "VARCHAR(64) NOT NULL DEFAULT ''",
|
||||
'token_exp' => 'TIMESTAMP DEFAULT 0',
|
||||
'language' => 'VARCHAR(2)',
|
||||
'timezone' => 'CHAR(64) NOT NULL', //see mysql.time_zone_name
|
||||
'clearance' => 'TINYINT(1) DEFAULT '.User::CLEARANCE_USER,
|
||||
'content' => 'LONGTEXT',
|
||||
'status' => 'VARCHAR(10)',
|
||||
'started_on'=> 'TIMESTAMP DEFAULT 0', //DEFAULT 0 removes auto-set to current time
|
||||
'closed_on' => 'TIMESTAMP DEFAULT 0'
|
||||
],
|
||||
'constraints' => [
|
||||
User::USER_TABLE => 'UNIQUE KEY `uni_email` (`email`)',
|
||||
//The book is always read as "this user's entries, in order",
|
||||
//so the reading order is indexed rather than the id alone.
|
||||
Journal::ENTRY_TABLE=> ['INDEX `idx_user_entry` (`id_user`, `id_entry`)', 'INDEX `idx_user_date` (`id_user`, `started_on`)']
|
||||
],
|
||||
//Deliberately no 'cascading_delete': Db cascades by reusing the same
|
||||
//id in the linked table, which would delete unrelated rows here.
|
||||
//The generated foreign keys already guard referential integrity.
|
||||
];
|
||||
}
|
||||
|
||||
/* Pages & API */
|
||||
|
||||
public function getAppMainPage(string $sCsrfToken = ''): string {
|
||||
$asViteAssets = $this->getViteAssets();
|
||||
|
||||
return parent::getMainPage(
|
||||
[
|
||||
'user' => $this->oUser->getUserInfo(),
|
||||
'consts' => [
|
||||
'title' => self::PROJECT_NAME,
|
||||
'languages' => self::LANGUAGES,
|
||||
'chunk_size' => Journal::CHUNK_SIZE,
|
||||
'default_timezone' => Settings::TIMEZONE,
|
||||
'autosave_delay' => 1200, //ms of stillness before a save
|
||||
'autosave_max_wait' => 8000, //ms of continuous typing before a forced save
|
||||
'csrf_token' => $sCsrfToken
|
||||
]
|
||||
],
|
||||
self::MAIN_PAGE,
|
||||
[
|
||||
'tags' => [
|
||||
'language' => $this->oLang->getLanguage(),
|
||||
'title' => self::PROJECT_NAME,
|
||||
'app_entry' => $asViteAssets['app']
|
||||
],
|
||||
'instances' => [
|
||||
'css' => $asViteAssets['css'],
|
||||
'module' => $asViteAssets['module']
|
||||
]
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function signup(string $sName, string $sEmail, string $sPassword, string $sTimezone): string {
|
||||
$asResult = $this->oUser->signup($sName, $sEmail, $sPassword, $this->oLang->getLanguage(), $sTimezone);
|
||||
return self::getJsonResult($asResult['result'], $asResult['desc_lang_id'], $asResult['data'], $asResult['desc_lang_params']);
|
||||
}
|
||||
|
||||
public function login(string $sEmail, string $sPassword, string $sTimezone, bool $bRemember): string {
|
||||
$asResult = $this->oUser->login($sEmail, $sPassword, $sTimezone, $bRemember);
|
||||
return self::getJsonResult($asResult['result'], $asResult['desc_lang_id'], $asResult['data'], $asResult['desc_lang_params']);
|
||||
}
|
||||
|
||||
public function logout(): string {
|
||||
$asResult = $this->oUser->logout();
|
||||
return self::getJsonResult($asResult['result'], $asResult['desc_lang_id'], $asResult['data']);
|
||||
}
|
||||
|
||||
public function updateAccount(string $sField, string $sValue): string {
|
||||
$asResult = $this->oUser->updateSettings($sField, $sValue);
|
||||
|
||||
//A language change has to reach the next page load's translations too.
|
||||
if($asResult['result'] && $sField == 'language') $this->oLang->setLanguage($this->oUser->getLang(), self::DEFAULT_LANG);
|
||||
|
||||
return self::getJsonResult($asResult['result'], $asResult['desc_lang_id'], $asResult['data'], $asResult['desc_lang_params']);
|
||||
}
|
||||
|
||||
/* Vite assets */
|
||||
|
||||
private function getViteAssets(): array {
|
||||
$sManifestPath = __DIR__.'/../public/.vite/manifest.json';
|
||||
|
||||
if(!file_exists($sManifestPath)) {
|
||||
$this->addError('Vite manifest not found - run "npm run dev" or "npm run prod" to build the frontend.');
|
||||
return ['app' => '', 'css' => [], 'module' => []];
|
||||
}
|
||||
|
||||
$asManifest = json_decode(file_get_contents($sManifestPath), true);
|
||||
$asAppImport = $asManifest[self::VITE_APP] ?? [];
|
||||
|
||||
//Recursive search for chunk imports
|
||||
$asImports = [];
|
||||
$asSeenImports = [self::VITE_APP => true];
|
||||
$this->appendViteImportedChunks($asManifest, $asAppImport, $asSeenImports, $asImports);
|
||||
|
||||
//CSS
|
||||
$asCssFiles = [];
|
||||
foreach(array_merge([$asAppImport], $asImports) as $asChunk) {
|
||||
foreach($asChunk['css'] ?? [] as $sCssFile) $asCssFiles[] = $sCssFile;
|
||||
}
|
||||
|
||||
//Modules
|
||||
$asModuleFiles = [];
|
||||
foreach($asImports as $asImport) {
|
||||
if(str_ends_with($asImport['file'] ?? '', '.js')) $asModuleFiles[] = $asImport['file'];
|
||||
}
|
||||
|
||||
return [
|
||||
'app' => $asAppImport['file'] ?? '',
|
||||
'css' => self::getViteAssetInstances($asCssFiles),
|
||||
'module' => self::getViteAssetInstances($asModuleFiles)
|
||||
];
|
||||
}
|
||||
|
||||
private function appendViteImportedChunks($asManifest, $asChunk, &$asSeenImports, &$asImports): void {
|
||||
foreach($asChunk['imports'] ?? [] as $sImport) {
|
||||
if(isset($asSeenImports[$sImport]) || !isset($asManifest[$sImport])) continue;
|
||||
|
||||
$asSeenImports[$sImport] = true;
|
||||
$this->appendViteImportedChunks($asManifest, $asManifest[$sImport], $asSeenImports, $asImports);
|
||||
$asImports[] = $asManifest[$sImport];
|
||||
}
|
||||
}
|
||||
|
||||
private static function getViteAssetInstances(array $asFilePaths): array {
|
||||
return array_map(static function($sFilePath) { return ['filename' => $sFilePath]; }, $asFilePaths);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user