From 8a77edbcea15002adc6f706e53aeda960f367f65 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 21 Apr 2026 10:52:17 +0200 Subject: [PATCH] speed up app --- assets/app.js | 2 + .../controllers/nostr_preview_controller.js | 2 +- assets/styles/a2hs.css | 10 +- assets/styles/app.css | 76 ++++++++++++ assets/styles/components/_nostr_previews.scss | 70 +++++++++-- assets/styles/event.css | 98 +++++++++++++++ assets/styles/layout.css | 18 ++- assets/styles/nostr-previews.css | 112 ++++++++++++++++++ assets/styles/og.css | 34 ++++++ assets/styles/theme.css | 12 +- assets/theme/default/theme.css | 6 +- src/Twig/Components/Organisms/Comments.php | 18 ++- .../Molecules/NostrPreview.html.twig | 10 +- .../Molecules/NostrPreviewContent.html.twig | 45 ++++--- .../components/Molecules/OgPreview.html.twig | 2 +- templates/event/index.html.twig | 85 ++----------- 16 files changed, 478 insertions(+), 122 deletions(-) create mode 100644 assets/styles/event.css create mode 100644 assets/styles/nostr-previews.css diff --git a/assets/app.js b/assets/app.js index 977d356..e3e3724 100644 --- a/assets/app.js +++ b/assets/app.js @@ -8,10 +8,12 @@ import './bootstrap.js'; import './styles/fonts.css'; import './styles/theme.css'; import './styles/app.css'; +import './styles/nostr-previews.css'; import './styles/layout.css'; import './styles/button.css'; import './styles/card.css'; import './styles/article.css'; +import './styles/event.css'; import './styles/og.css'; import './styles/form.css'; import './styles/notice.css'; diff --git a/assets/controllers/nostr_preview_controller.js b/assets/controllers/nostr_preview_controller.js index b69d88b..8256808 100644 --- a/assets/controllers/nostr_preview_controller.js +++ b/assets/controllers/nostr_preview_controller.js @@ -16,7 +16,7 @@ export default class extends Controller { async fetchPreview() { try { - this.containerTarget.innerHTML = '
Loading preview...
'; + this.containerTarget.innerHTML = '
Loading preview…
'; if (this.typeValue === 'url' && this.fullMatchValue) { // Fetch OG preview for plain URLs fetch("/og-preview/", { diff --git a/assets/styles/a2hs.css b/assets/styles/a2hs.css index a6246fb..27febf9 100644 --- a/assets/styles/a2hs.css +++ b/assets/styles/a2hs.css @@ -2,11 +2,19 @@ position: fixed; bottom: 20px; left: 20px; - background: #ffffff; + background: var(--color-bg); + color: var(--color-text); border: 1px solid var(--color-border); padding: 1rem; border-radius: 8px; z-index: 1000; + box-shadow: 0 4px 14px var(--color-shadow); +} + +.install-prompt-box a:focus-visible, +.install-prompt-box button:focus-visible { + outline: 2px solid var(--color-focus-ring); + outline-offset: 2px; } .install-prompt-box.hidden { diff --git a/assets/styles/app.css b/assets/styles/app.css index 979e5e0..a269bc6 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -647,6 +647,82 @@ label.search { margin-bottom: 15px; } +/* Shared layout / theme utilities (replaces Bootstrap-only classes where removed) */ +.text-subtle { + color: var(--color-text-mid); +} + +.text-muted { + color: var(--color-text-mid); +} + +.text-center { + text-align: center; +} + +.my-2 { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.ms-2 { + margin-left: 0.5rem; +} + +.nostr-card-header { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem; +} + +.ui-badge { + display: inline-block; + padding: 0.2rem 0.55rem; + font-size: 0.75rem; + font-weight: 600; + line-height: 1.2; + border-radius: 4px; + letter-spacing: 0.02em; +} + +.ui-badge--brand { + background-color: var(--brand-color); + color: var(--color-text-contrast); +} + +.ui-badge--primary { + background-color: var(--color-primary); + color: var(--color-text-contrast); +} + +.ui-badge--secondary { + background-color: var(--color-secondary); + color: var(--color-text-contrast); +} + +.ui-badge--neutral { + background-color: var(--color-bg-light); + color: var(--color-text); + border: 1px solid var(--color-border); +} + +.alert.alert-warning { + background-color: var(--color-bg-light); + color: var(--color-text); + border: 1px solid var(--color-border); +} + +a:focus-visible { + outline: 2px solid var(--color-focus-ring); + outline-offset: 2px; +} + @media (max-width: 600px) { h1.brand { font-size: 2.2rem; diff --git a/assets/styles/components/_nostr_previews.scss b/assets/styles/components/_nostr_previews.scss index fe3dc65..b7c0c80 100644 --- a/assets/styles/components/_nostr_previews.scss +++ b/assets/styles/components/_nostr_previews.scss @@ -1,26 +1,71 @@ +.nostr-preview__loading { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 0.25rem; +} + +.nostr-preview__spinner { + box-sizing: border-box; + width: 1.25rem; + height: 1.25rem; + border: 2px solid var(--color-border); + border-top-color: var(--color-primary); + border-radius: 50%; + animation: nostr-preview-spin 0.75s linear infinite; +} + +@keyframes nostr-preview-spin { + to { + transform: rotate(360deg); + } +} + +.nostr-preview__loading-text { + color: var(--color-text-mid); + font-size: 0.9rem; +} + .nostr-preview { margin-top: 0.5rem; - .nostr-event-preview, .nostr-profile-preview { - border-left: 3px solid #6c5ce7; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + .nostr-event-preview, + .nostr-profile-preview, + .nostr-address-preview, + .nostr-highlight-preview { + border-left: 3px solid var(--color-primary); + box-shadow: 0 2px 6px var(--color-shadow); } .nostr-profile-preview { - border-left-color: #00b894; + border-left-color: var(--color-secondary); + } + + .nostr-highlight-preview { + border-left-color: var(--brand-color); } .card-title { margin-bottom: 0.5rem; font-size: 1rem; + color: var(--color-text); } .card-text { font-size: 0.9rem; + color: var(--color-text); } .card-footer { padding: 0.5rem 1rem; + color: var(--color-text-mid); + } + + .nostr-profile-preview__body { + display: flex; + flex-direction: column; + gap: 0.35rem; } } @@ -28,25 +73,32 @@ h6 { font-size: 0.9rem; margin-bottom: 1rem; + color: var(--color-text); } .preview-container { - // For multiple previews max-height: 500px; overflow-y: auto; padding-right: 10px; } } -// Style for nostr links in text +/* In-content nostr: / npub links (CommonMark) */ .nostr-link { - color: #6c5ce7; - background-color: rgba(108, 92, 231, 0.1); + color: var(--color-link); + background-color: color-mix(in srgb, var(--color-primary) 18%, transparent); padding: 0 3px; border-radius: 3px; text-decoration: none; + font-weight: 500; &:hover { - background-color: rgba(108, 92, 231, 0.2); + background-color: color-mix(in srgb, var(--color-primary) 24%, transparent); + color: var(--color-link-hover); + } + + &:focus-visible { + outline: 2px solid var(--color-focus-ring); + outline-offset: 2px; } } diff --git a/assets/styles/event.css b/assets/styles/event.css new file mode 100644 index 0000000..b10cc49 --- /dev/null +++ b/assets/styles/event.css @@ -0,0 +1,98 @@ +/* Nostr event detail page (EventController) */ +.event-page { + max-width: 800px; + margin: 2rem auto; + padding: 1.5rem; + background-color: var(--color-bg); + color: var(--color-text); + border: 1px solid var(--color-border); + border-radius: 8px; + box-shadow: 0 2px 8px var(--color-shadow); +} + +.event-page__header { + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-wrap: wrap; + gap: 1rem; + margin-bottom: 1.5rem; + border-bottom: 1px solid var(--color-border); + padding-bottom: 1rem; +} + +.event-page__content { + font-size: 1.1rem; + line-height: 1.6; + margin-bottom: 2rem; + white-space: pre-wrap; + color: var(--color-text); +} + +.event-page__links { + margin: 1.5rem 0; + padding: 1rem; + background-color: var(--color-bg-light); + color: var(--color-text); + border: 1px solid var(--color-border); + border-radius: 6px; +} + +.event-page__links h4 { + margin: 0 0 0.75rem; + font-size: 1.05rem; + font-weight: 600; + color: var(--color-text); +} + +.event-page__links .link-list { + list-style: none; + padding-left: 0; +} + +.event-page__links .link-list li { + margin-bottom: 0.5rem; + word-break: break-all; +} + +.event-page__link-type { + color: var(--color-text-mid); + font-size: 0.9rem; + margin-left: 0.5rem; +} + +.event-page__footer { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 1rem; + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid var(--color-border); +} + +.event-page__tags { + flex: 1; + color: var(--color-text); +} + +.event-page__tags ul, +.event-page__references ul { + list-style-type: none; + padding-left: 0; +} + +.event-page__tags li, +.event-page__references li { + margin-bottom: 0.5rem; +} + +.event-page__meta { + color: var(--color-text-mid); + font-size: 0.95rem; +} + +.event-page a:focus-visible { + outline: 2px solid var(--color-focus-ring); + outline-offset: 2px; +} diff --git a/assets/styles/layout.css b/assets/styles/layout.css index d0bd88e..dff9ba9 100644 --- a/assets/styles/layout.css +++ b/assets/styles/layout.css @@ -170,12 +170,13 @@ dt { /* Footer */ footer { - background-color: #333; - color: white; + background-color: var(--color-footer-bg); + color: var(--color-footer-text); text-align: center; padding: 1em 0; position: relative; width: 100%; + border-top: 1px solid var(--color-border); } footer .footer-links { @@ -183,7 +184,18 @@ footer .footer-links { } .footer-links a { - color: var(--color-text-light); + color: var(--color-footer-link); + text-decoration: underline; + text-underline-offset: 2px; +} + +.footer-links a:hover { + color: var(--color-text); +} + +.footer-links a:focus-visible { + outline: 2px solid var(--color-focus-ring); + outline-offset: 3px; } .search input { diff --git a/assets/styles/nostr-previews.css b/assets/styles/nostr-previews.css new file mode 100644 index 0000000..83b7451 --- /dev/null +++ b/assets/styles/nostr-previews.css @@ -0,0 +1,112 @@ +/* Nostr link previews + in-content nostr: links (mirrors components/_nostr_previews.scss; loaded by app.js) */ +.nostr-preview__loading { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 0.25rem; +} + +.nostr-preview__spinner { + box-sizing: border-box; + width: 1.25rem; + height: 1.25rem; + border: 2px solid var(--color-border); + border-top-color: var(--color-primary); + border-radius: 50%; + animation: nostr-preview-spin 0.75s linear infinite; +} + +@keyframes nostr-preview-spin { + to { + transform: rotate(360deg); + } +} + +.nostr-preview__loading-text { + color: var(--color-text-mid); + font-size: 0.9rem; +} + +.nostr-preview { + margin-top: 0.5rem; +} + +.nostr-preview .nostr-preview-link a { + color: var(--color-link); + text-decoration: underline; + text-underline-offset: 2px; +} + +.nostr-preview .nostr-preview-link a:hover { + color: var(--color-link-hover); +} + +.nostr-preview .nostr-event-preview, +.nostr-preview .nostr-profile-preview, +.nostr-preview .nostr-address-preview, +.nostr-preview .nostr-highlight-preview { + border-left: 3px solid var(--color-primary); + box-shadow: 0 2px 6px var(--color-shadow); +} + +.nostr-preview .nostr-profile-preview { + border-left-color: var(--color-secondary); +} + +.nostr-preview .nostr-highlight-preview { + border-left-color: var(--brand-color); +} + +.nostr-preview .card-title { + margin-bottom: 0.5rem; + font-size: 1rem; + color: var(--color-text); +} + +.nostr-preview .card-text { + font-size: 0.9rem; + color: var(--color-text); +} + +.nostr-preview .card-footer { + padding: 0.5rem 1rem; + color: var(--color-text-mid); +} + +.nostr-preview .nostr-profile-preview__body { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.nostr-previews h6 { + font-size: 0.9rem; + margin-bottom: 1rem; + color: var(--color-text); +} + +.nostr-previews .preview-container { + max-height: 500px; + overflow-y: auto; + padding-right: 10px; +} + +.nostr-link { + color: var(--color-link); + background-color: color-mix(in srgb, var(--color-primary) 18%, transparent); + padding: 0 3px; + border-radius: 3px; + text-decoration: none; + font-weight: 500; +} + +.nostr-link:hover { + background-color: color-mix(in srgb, var(--color-primary) 24%, transparent); + color: var(--color-link-hover); +} + +.nostr-link:focus-visible { + outline: 2px solid var(--color-focus-ring); + outline-offset: 2px; +} diff --git a/assets/styles/og.css b/assets/styles/og.css index d5985ef..1adb148 100644 --- a/assets/styles/og.css +++ b/assets/styles/og.css @@ -3,9 +3,43 @@ padding: 20px; margin: 10px 0; background-color: var(--color-bg); + color: var(--color-text); + border: 1px solid var(--color-border); + border-radius: 6px; } .og-preview-image { width: 100%; height: auto; } + +.og-preview-content a { + color: var(--color-link); + text-decoration: underline; + text-underline-offset: 2px; +} + +.og-preview-content a:hover { + color: var(--color-link-hover); +} + +.og-preview-content a:focus-visible { + outline: 2px solid var(--color-focus-ring); + outline-offset: 2px; +} + +.og-preview-title a { + color: var(--color-text); + text-decoration: none; + font-weight: 700; +} + +.og-preview-title a:hover { + color: var(--color-primary); + text-decoration: underline; +} + +.og-preview-description { + color: var(--color-text-mid); + margin-top: 0.35rem; +} diff --git a/assets/styles/theme.css b/assets/styles/theme.css index 60e4570..1963e89 100644 --- a/assets/styles/theme.css +++ b/assets/styles/theme.css @@ -9,15 +9,25 @@ --color-bg-light: #2a2a2a; /* Slightly lighter charcoal */ --color-bg-primary: #2e1f2e; /* Muted aubergine for a rich, elegant feel */ --color-text: #f5f5f5; /* Soft white for readability */ - --color-text-mid: #d8d8d8; /* Warm light gray */ + --color-text-mid: #d8d8d8; /* Warm light gray — use on dark bg for ≥4.5:1 vs --color-bg */ --color-text-contrast: #000; /* Black text for contrast */ --color-primary: #5F7355; /* Plum primary color */ --color-secondary: #495544; /* secondary color */ --color-border: #3a3a3a; /* Subtle gray border */ + /* Aliases / derived (WCAG AA on typical surfaces when paired as documented) */ + --color-text-light: var(--color-text-mid); /* deprecated name: use --color-text-mid */ + --color-footer-bg: var(--color-bg-light); + --color-footer-text: var(--color-text); + --color-footer-link: var(--color-primary); + --color-link: var(--color-secondary); + --color-link-hover: var(--color-text); + --color-focus-ring: var(--color-secondary); + --color-shadow: color-mix(in srgb, var(--color-text) 16%, transparent); --font-family: 'Montserrat', serif; /* Set the Montserrat font as default */ --main-body-font: 'Newsreader', serif; /* Set the font for the main body */ --heading-font: 'EB Garamond', serif; /* Set the font for headings */ --brand-font: 'Lobster', serif; /* A classic, refined branding font */ --brand-color: white; + --accent-color: var(--color-secondary); } diff --git a/assets/theme/default/theme.css b/assets/theme/default/theme.css index 3953af7..7eef11b 100644 --- a/assets/theme/default/theme.css +++ b/assets/theme/default/theme.css @@ -2,7 +2,11 @@ --color-bg: #f4f1ee; --color-bg-light: #e8e4df; --color-text: #2a2a2a; - --color-text-mid: #3a3a3a; /* Warm light gray */ + --color-text-mid: #3d3a36; --color-text-contrast: #f4f1ee; --brand-color: black; + --color-link: #3a342f; + --color-link-hover: #1a1a1a; + --color-focus-ring: #1a1a1a; + --color-shadow: color-mix(in srgb, var(--color-text) 10%, transparent); } diff --git a/src/Twig/Components/Organisms/Comments.php b/src/Twig/Components/Organisms/Comments.php index cdb3db0..1acd626 100644 --- a/src/Twig/Components/Organisms/Comments.php +++ b/src/Twig/Components/Organisms/Comments.php @@ -4,19 +4,23 @@ namespace App\Twig\Components\Organisms; use App\Service\NostrClient; use App\Service\NostrLinkParser; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; #[AsTwigComponent] final class Comments { public array $list = []; + public array $commentLinks = []; + public array $processedContent = []; public function __construct( private readonly NostrClient $nostrClient, - private readonly NostrLinkParser $nostrLinkParser - + private readonly NostrLinkParser $nostrLinkParser, + private readonly CacheInterface $cache, ) { } @@ -25,10 +29,14 @@ final class Comments */ public function mount($current): void { - // Fetch comments - $this->list = $this->nostrClient->getComments($current); + $cacheKey = 'comments_' . hash('sha256', (string) $current); + + $this->list = $this->cache->get($cacheKey, function (ItemInterface $item) use ($current) { + $item->expiresAfter(120); + + return $this->nostrClient->getComments($current); + }); - // Parse Nostr links in comments but don't fetch previews $this->parseNostrLinks(); } diff --git a/templates/components/Molecules/NostrPreview.html.twig b/templates/components/Molecules/NostrPreview.html.twig index bf49ef3..3c1c0cd 100644 --- a/templates/components/Molecules/NostrPreview.html.twig +++ b/templates/components/Molecules/NostrPreview.html.twig @@ -5,9 +5,9 @@ data-nostr-preview-decoded-value="{{ preview.data|json_encode }}" data-nostr-preview-full-match-value="{{ preview.full_match }}">
-
-
- Loading preview... +
+ + Loading preview…
{% if preview.full_match is defined and preview.full_match %} diff --git a/templates/components/Molecules/NostrPreviewContent.html.twig b/templates/components/Molecules/NostrPreviewContent.html.twig index c9d7918..d4985b1 100644 --- a/templates/components/Molecules/NostrPreviewContent.html.twig +++ b/templates/components/Molecules/NostrPreviewContent.html.twig @@ -12,14 +12,13 @@ {% endfor %}
{% elseif preview.type == 'nevent' %} - {# If kind is 9802 - Highlight #} {% if preview.kind == 9802 %}
-
+
- Highlight + Highlight

{{ preview.content }}

@@ -40,41 +39,53 @@
{% else %} + {% set is_longform = preview.kind == 30023 or preview.kind == 30024 %} + {% set article_title = null %} + {% set article_summary = null %} + {% if preview.tags is defined %} + {% for tag in preview.tags %} + {% if tag[0] == 'title' %} + {% set article_title = tag[1] %} + {% elseif tag[0] == 'summary' %} + {% set article_summary = tag[1] %} + {% endif %} + {% endfor %} + {% endif %}
-
+
- +
- {% if preview.type == 'event' %} - Article + {% if is_longform %} + Article {% else %} - Note + Note {% endif %}
- {% if preview.type == 'event' %} -
{{ preview.title }}
+ {% if is_longform %} +
{{ article_title ?: 'Article' }}

- {% if preview.event.summary is defined %} - {{ preview.event.summary }} + {% if article_summary %} + {{ article_summary }} {% else %} - {{ preview.event.content|length > 150 ? preview.event.content|slice(0, 150) ~ '...' : preview.event.content }} + {{ preview.content|length > 150 ? preview.content|slice(0, 150) ~ '...' : preview.content }} {% endif %}

{% else %}

{{ preview.content|length > 280 ? preview.content|slice(0, 280) ~ '...' : preview.content }}

{% endif %}
- {% endif %} {% elseif preview.type == 'nprofile' %}
-
+
{{ preview.display_name ?: preview.name }}
- @{{ preview.npub|shortenNpub }} + @{{ preview.npub|shortenNpub }} {% if preview.about %}

{{ preview.about|length > 150 ? preview.about|slice(0, 150) ~ '...' : preview.about }}

{% endif %} diff --git a/templates/components/Molecules/OgPreview.html.twig b/templates/components/Molecules/OgPreview.html.twig index 72d2ed6..55e5152 100644 --- a/templates/components/Molecules/OgPreview.html.twig +++ b/templates/components/Molecules/OgPreview.html.twig @@ -14,7 +14,7 @@ {{ og.title ?: og.url }}
{% if og.description %} -
{{ og.description }}
+
{{ og.description|e }}
{% endif %}
diff --git a/templates/event/index.html.twig b/templates/event/index.html.twig index c0c11e9..fe7336f 100644 --- a/templates/event/index.html.twig +++ b/templates/event/index.html.twig @@ -4,8 +4,8 @@ {% block body %}
-
-
+
+
{% if author %} {% set author_pic = null %} {% if author.picture is defined and author.picture %} @@ -27,30 +27,30 @@

{% endif %} -
+
{{ event.created_at|date('F j, Y - H:i') }}
-
+
{% if nostrLinks is defined and nostrLinks|length > 0 %} -