Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,32 @@ ones are marked like "v1.0.0-fork".

## [Unreleased]

### Changed in Unreleased

* **The sign-in pages are one centred card** on every screen size. Login,
registration and password recovery no longer repeat the logo and title
above a narrow form.

### Fixed in Unreleased

* The "Email not verified" banner no longer follows accounts that have no
email address.

* Opening the Add Text page no longer loads every suggested Gutenberg book
in the background, and book previews have their own request quota, so
they cannot make saving a text fail with "Too Many Requests".

* Terms added from the reader now count towards "terms created", and a text
counts as read once a day rather than on every page load.

* A new term's translation appears under the word straight away, not after
a reload.

* Dark theme: readable reader annotations, home cards and starter vocabulary
cards, and a darker background for empty days on the activity calendar.

* The Anki import pages no longer show their heading twice.

* **New languages lost their settings.** The language wizard filled the form
on screen but not the form's state, so Save reported a missing name, and
retyping the name stored the language without its dictionary, language
Expand Down
8 changes: 4 additions & 4 deletions src/Modules/Home/Views/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class="delete"
<!-- Current text card -->
<div style="flex-shrink: 0;">
<template x-if="lastText">
<div class="box has-background-link-light" style="width: 280px; min-height: 180px;">
<div class="box has-background-link-soft" style="width: 280px; min-height: 180px;">
<p dir="auto" class="title is-5 mb-3" x-text="lastText.title"></p>
<!-- Statistics bar - colors match word status highlights -->
<div
Expand Down Expand Up @@ -194,7 +194,7 @@ class="button is-success is-light is-small"
<div style="flex-shrink: 0;">
<a
href="<?php echo $base; ?>/texts/new"
class="box has-background-primary-light has-text-centered"
class="box has-background-primary-soft has-text-centered"
style="width: 180px; min-height: 180px; display: flex;
flex-direction: column; justify-content: center; align-items: center;"
>
Expand All @@ -209,12 +209,12 @@ class="box has-background-primary-light has-text-centered"
<div style="flex-shrink: 0;">
<div
data-action="open-library-search"
class="box has-background-warning-light has-text-centered"
class="box has-background-warning-soft has-text-centered"
style="width: 180px; min-height: 180px; display: flex;
flex-direction: column; justify-content: center; align-items: center;
cursor: pointer;"
>
<span class="icon is-large has-text-warning-dark">
<span class="icon is-large has-text-warning">
<i data-lucide="search" style="width: 48px; height: 48px;"></i>
</span>
<p class="mt-3 has-text-weight-semibold"><?= __('home.search_library') ?></p>
Expand Down
28 changes: 27 additions & 1 deletion src/Modules/Text/Http/TextReadController.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,32 @@ public function getTextIdFromRequest(?int $injectedId = null): ?int
return null;
}

/**
* Count a text as read once per day, however often its page is opened.
*
* @param int $textId Text ID
*
* @return void
*/
private function countTextAsRead(int $textId): void
{
$today = date('Y-m-d');
/** @var mixed $counted */
$counted = $_SESSION['texts_read_counted'] ?? null;
if (!is_array($counted) || ($counted['date'] ?? null) !== $today) {
$counted = ['date' => $today, 'ids' => []];
}
/** @var list<int> $ids */
$ids = is_array($counted['ids'] ?? null) ? $counted['ids'] : [];
if (in_array($textId, $ids, true)) {
return;
}

$ids[] = $textId;
$_SESSION['texts_read_counted'] = ['date' => $today, 'ids' => $ids];
(new MySqlActivityRepository())->incrementTextsRead();
}

/**
* Render the text reading page.
*
Expand All @@ -110,7 +136,7 @@ public function renderReadPage(int $textId): ?RedirectResponse
return $this->redirect('/text/edit');
}

(new MySqlActivityRepository())->incrementTextsRead();
$this->countTextAsRead($textId);

$title = (string) $headerData['TxTitle'];
$langId = (int) $headerData['TxLgID'];
Expand Down
12 changes: 12 additions & 0 deletions src/Modules/User/Domain/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,18 @@ public function isEmailVerified(): bool
return $this->emailVerifiedAt !== null;
}

/**
* Whether the user has an email address still waiting for verification.
*
* Email is optional, and an account without one has nothing to verify.
*
* @return bool
*/
public function needsEmailVerification(): bool
{
return $this->email !== null && !$this->isEmailVerified();
}

/**
* Mark the email as verified.
*
Expand Down
2 changes: 1 addition & 1 deletion src/Modules/User/Views/profile.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
<?php echo $t('profile.email_verified'); ?>
</div>
<div class="notification is-warning is-light is-size-7 py-2 px-3 mb-4"
x-show="!emailVerified" x-cloak>
x-show="showsUnverifiedNotice()" x-cloak>
<?php echo $t('profile.email_not_verified'); ?>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

namespace Lwt\Modules\Vocabulary\Application\Services;

use Lwt\Modules\Activity\Infrastructure\MySqlActivityRepository;
use Lwt\Shared\Infrastructure\Utilities\StringUtils;
use Lwt\Shared\Infrastructure\Database\Connection;
use Lwt\Shared\Infrastructure\Database\Escaping;
Expand Down Expand Up @@ -206,6 +207,8 @@ public function insertWordWithStatus(int $textId, string $term, int $status): ar
. ")";

$wid = (int) Connection::preparedInsert($sql, $bindings);

(new MySqlActivityRepository())->incrementTermsCreated();
} else {
/** @var list<int|string> $updateBindings */
$updateBindings = [$status, $wid];
Expand Down
3 changes: 3 additions & 0 deletions src/Modules/Vocabulary/Http/TermCrudApiHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Lwt\Shared\Infrastructure\Database\Maintenance;
use Lwt\Shared\Infrastructure\Database\QueryBuilder;
use Lwt\Shared\Infrastructure\Database\UserScopedQuery;
use Lwt\Modules\Activity\Infrastructure\MySqlActivityRepository;
use Lwt\Modules\Vocabulary\Application\VocabularyFacade;
use Lwt\Modules\Vocabulary\Application\UseCases\FindSimilarTerms;
use Lwt\Modules\Vocabulary\Application\Services\TermStatusService;
Expand Down Expand Up @@ -815,6 +816,8 @@ public function createTermFull(array $data): array

$wordId = $stmt->insertId();

(new MySqlActivityRepository())->incrementTermsCreated();

// Update text items to link to this word
// word_occurrences inherits user context via Ti2TxID -> texts FK
Connection::preparedExecute(
Expand Down
1 change: 0 additions & 1 deletion src/Modules/Vocabulary/Views/anki_deck_mapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@
};

?>
<h1><?= __e('vocabulary.anki.deck.title') ?></h1>

<?php if ($error !== null) { ?>
<div class="notification is-danger"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div>
Expand Down
1 change: 0 additions & 1 deletion src/Modules/Vocabulary/Views/anki_deck_result.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
};

?>
<h1><?= __e('vocabulary.anki.deck.title') ?></h1>

<div class="notification is-success">
<p><strong><?= __ne('vocabulary.anki.deck.imported', $result->created) ?></strong></p>
Expand Down
1 change: 0 additions & 1 deletion src/Modules/Vocabulary/Views/anki_deck_upload.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
assert($error === null || is_string($error));

?>
<h1><?= __e('vocabulary.anki.deck.title') ?></h1>

<?php if ($error !== null) { ?>
<div class="notification is-danger"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div>
Expand Down
1 change: 0 additions & 1 deletion src/Modules/Vocabulary/Views/apkg_import.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@
: 'vocabulary.anki.apkg.settings_export';

?>
<h1><?= __e('vocabulary.anki.apkg.title') ?></h1>

<?php if ($error !== null) { ?>
<div class="notification is-danger"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div>
Expand Down
4 changes: 2 additions & 2 deletions src/Modules/Vocabulary/Views/starter_vocab.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
<?php if ($isAvailable) : ?>
<!-- Wiktionary frequency words source -->
<label class="box mb-3 p-4" style="cursor: pointer;"
:class="useWiktionary ? 'has-background-success-light' : ''">
:class="useWiktionary ? 'has-background-success-soft' : ''">
<div class="is-flex is-align-items-center">
<input type="checkbox" class="me-3"
:checked="useWiktionary"
Expand Down Expand Up @@ -129,7 +129,7 @@
<!-- Curated dictionaries -->
<template x-for="source in dictSources" :key="source.name">
<label class="box mb-3 p-4" style="cursor: pointer;"
:class="isSourceSelected(source.url) ? 'has-background-success-light' : ''">
:class="isSourceSelected(source.url) ? 'has-background-success-soft' : ''">
<div class="is-flex is-align-items-center">
<input type="checkbox" class="me-3"
:checked="isSourceSelected(source.url)"
Expand Down
2 changes: 1 addition & 1 deletion src/Modules/Vocabulary/Views/upload_form.php
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ class="mb-3">
<h3 class="title is-5 mb-3" x-text="group.languageName"></h3>
<template x-for="source in group.sources" :key="source.name">
<label class="box mb-3 p-4" style="cursor: pointer;"
:class="isSelected(source.url) ? 'has-background-success-light' : ''">
:class="isSelected(source.url) ? 'has-background-success-soft' : ''">
<div class="is-flex is-align-items-center">
<input type="checkbox" class="me-3"
:checked="isSelected(source.url)"
Expand Down
6 changes: 5 additions & 1 deletion src/Shared/Http/BaseController.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ public function __construct()
*/
protected function render(string $title, bool $showMenu = true): void
{
PageLayoutHelper::renderPageStart($title, $showMenu);
if ($showMenu) {
PageLayoutHelper::renderPageStart($title, true);
return;
}
PageLayoutHelper::renderAuthPageStart($title);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ private function getClientIdentifier(): string
/**
* Get the endpoint type for rate limiting.
*
* @return string Endpoint type: 'auth', 'whisper', or 'api'
* @return string Endpoint type: 'auth', 'whisper', 'preview', or 'api'
*/
private function getEndpointType(): string
{
Expand All @@ -223,6 +223,13 @@ private function getEndpointType(): string
return 'whisper';
}

// Library previews arrive in bursts, one per suggested book. They keep
// the default limit but count separately, so browsing books cannot
// use up the quota that saving a text needs.
if (preg_match('#/api(?:\.php)?/v1/texts/library-preview$#', $requestPath)) {
return 'preview';
}

return 'api';
}

Expand Down
23 changes: 20 additions & 3 deletions src/Shared/UI/Helpers/PageLayoutHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,22 @@ public static function renderPageStart(string $title, bool $close, string $curre
echo self::buildPageTitle($title);
}

/**
* Start a sign-in page.
*
* These pages carry their own logo and heading inside a centred card, so
* they take neither the page logo nor the page title.
*
* @param string $title Browser-tab title
*
* @return void
*/
public static function renderAuthPageStart(string $title): void
{
self::renderPageStartNobody($title, 'auth-page');
echo '<main id="main-content">';
}

/**
* Build a compact UI-language switcher for the pre-login pages.
*
Expand Down Expand Up @@ -860,8 +876,9 @@ public static function renderPageEnd(): void
/**
* Render a warning banner when the current user's email is not verified.
*
* Only shows in multi-user mode when the user is authenticated
* but has not yet verified their email address.
* Only shows in multi-user mode when the user is authenticated and has an
* email address they have not yet verified. Email is optional at
* registration, and an account without one has nothing to verify.
*
* @return void
*/
Expand All @@ -879,7 +896,7 @@ public static function renderEmailVerificationBanner(): void
return;
}

if ($user === null || $user->isEmailVerified()) {
if ($user === null || !$user->needsEmailVerification()) {
return;
}

Expand Down
47 changes: 47 additions & 0 deletions src/frontend/css/base/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -3306,3 +3306,50 @@ Searchable Select Component (Alpine.js)
max-height: 190px;
}
}

/* Reader annotations: the #006699 set in text_styles.ts is unreadable on black */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .word-ann {
color: #7cc4ff;
}
}

:root[data-theme="dark"] .word-ann {
color: #7cc4ff;
}

/* Sign-in pages: one centred card, on every screen size */
.auth-page main {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 100vh;
min-height: 100dvh;
padding: 0;
}

.auth-page .section {
padding: 1.5rem 1rem;
}

.auth-page .section > .container {
padding: 0;
}

.auth-page .section > .container > .columns {
display: flex;
justify-content: center;
margin: 0;
}

.auth-page .section > .container > .columns > .column {
flex: 0 1 26rem;
max-width: 26rem;
padding: 0;
width: 100%;
}

.auth-page .box h1.title {
border-bottom: none;
padding-bottom: 0;
}
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,13 @@ export function calendarHeatmap(): CalendarHeatmapState {
rect.setAttribute('width', String(CELL_SIZE));
rect.setAttribute('height', String(CELL_SIZE));
rect.setAttribute('rx', '2');
rect.setAttribute('fill', COLORS[colorLevel(day.total, maxVal)]);
const level = colorLevel(day.total, maxVal);
rect.setAttribute('fill', COLORS[level]);
if (level === 0) {
// A fill attribute cannot read a CSS variable, and this one follows
// the theme, so that a dark page does not show a wall of white.
rect.style.fill = 'var(--bulma-border, #ebedf0)';
}

const label = formatTooltip(day);
rect.addEventListener('mouseenter', (e: MouseEvent) => {
Expand Down
Loading
Loading