mirror of
https://codeberg.org/danielsiepmann/news-reader-php.git
synced 2024-11-24 04:56:09 +01:00
WIP|Allow to mark entries as read and unread
Add new read link to each entry. If entry was read the link will be replaced with an unread link. Clicking the link will mark the entry as read or unread. Afterwards a redirect to referrer happens. If it is unkown, for whatever reason, fallback to start. Flash messages are added and rendered to let user know what happened. Also respect already read entries when opening a feed. WIP: Still need to adjust entry listing for buckets. Still need to add tests for new feature. Still need to add new start page to show newest unread entries from all buckets.
This commit is contained in:
parent
bed87f8a10
commit
a4e4f5da0d
14 changed files with 146 additions and 11 deletions
|
@ -3,6 +3,10 @@
|
||||||
--color-foreground: #D3D7CF;
|
--color-foreground: #D3D7CF;
|
||||||
--color-blue-light: #9CD9F0;
|
--color-blue-light: #9CD9F0;
|
||||||
--color-blue-dark: #72B3CC;
|
--color-blue-dark: #72B3CC;
|
||||||
|
--color-green-light: #CDEE69;
|
||||||
|
--color-green-dark: #8EB33B;
|
||||||
|
--color-yellow-light: #FFE377;
|
||||||
|
--color-yellow-dark: #D0B03C;
|
||||||
--color-black-light: #5D5D5D;
|
--color-black-light: #5D5D5D;
|
||||||
--color-black-dark: #000000;
|
--color-black-dark: #000000;
|
||||||
--color-white-light: #F7F7F7;
|
--color-white-light: #F7F7F7;
|
||||||
|
|
|
@ -5,4 +5,5 @@ body > main {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@import 'content/flash';
|
||||||
@import 'content/entries';
|
@import 'content/entries';
|
||||||
|
|
20
assets/sass/components/content/_flash.scss
Normal file
20
assets/sass/components/content/_flash.scss
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
body > aside {
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
color: var(--color-black-dark);
|
||||||
|
margin: var(--spacing-elements);
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding: var(--spacing-small-elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
background-color: var(--color-green-dark);
|
||||||
|
border: var(--color-green-light) solid var(--width-border-default);
|
||||||
|
}
|
||||||
|
&.notice {
|
||||||
|
background-color: var(--color-yellow-dark);
|
||||||
|
border: var(--color-yellow-light) solid var(--width-border-default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,3 +17,11 @@ feed:
|
||||||
entry:
|
entry:
|
||||||
path: /entry/{slug}
|
path: /entry/{slug}
|
||||||
controller: App\Controller\EntryController::show
|
controller: App\Controller\EntryController::show
|
||||||
|
|
||||||
|
entry-mark-as-read:
|
||||||
|
path: /entry/{slug}/mark/read
|
||||||
|
controller: App\Controller\EntryController::read
|
||||||
|
|
||||||
|
entry-mark-as-un-read:
|
||||||
|
path: /entry/{slug}/mark/un-read
|
||||||
|
controller: App\Controller\EntryController::unRead
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
--color-foreground: #D3D7CF;
|
--color-foreground: #D3D7CF;
|
||||||
--color-blue-light: #9CD9F0;
|
--color-blue-light: #9CD9F0;
|
||||||
--color-blue-dark: #72B3CC;
|
--color-blue-dark: #72B3CC;
|
||||||
|
--color-green-light: #CDEE69;
|
||||||
|
--color-green-dark: #8EB33B;
|
||||||
|
--color-yellow-light: #FFE377;
|
||||||
|
--color-yellow-dark: #D0B03C;
|
||||||
--color-black-light: #5D5D5D;
|
--color-black-light: #5D5D5D;
|
||||||
--color-black-dark: #000000;
|
--color-black-dark: #000000;
|
||||||
--color-white-light: #F7F7F7;
|
--color-white-light: #F7F7F7;
|
||||||
|
@ -56,6 +60,19 @@ body > main {
|
||||||
body > main header h1 {
|
body > main header h1 {
|
||||||
margin: 0; }
|
margin: 0; }
|
||||||
|
|
||||||
|
body > aside ul {
|
||||||
|
list-style: none;
|
||||||
|
color: var(--color-black-dark);
|
||||||
|
margin: var(--spacing-elements); }
|
||||||
|
body > aside ul li {
|
||||||
|
padding: var(--spacing-small-elements); }
|
||||||
|
body > aside ul.success {
|
||||||
|
background-color: var(--color-green-dark);
|
||||||
|
border: var(--color-green-light) solid var(--width-border-default); }
|
||||||
|
body > aside ul.notice {
|
||||||
|
background-color: var(--color-yellow-dark);
|
||||||
|
border: var(--color-yellow-light) solid var(--width-border-default); }
|
||||||
|
|
||||||
article {
|
article {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 50em var(--width-sidebar-max);
|
grid-template-columns: 50em var(--width-sidebar-max);
|
||||||
|
|
|
@ -3,14 +3,64 @@
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\Entry;
|
use App\Entity\Entry;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
class EntryController extends AbstractController
|
class EntryController extends AbstractController
|
||||||
{
|
{
|
||||||
public function show(Entry $entry)
|
/**
|
||||||
|
* @var EntityManagerInterface
|
||||||
|
*/
|
||||||
|
private $entityManager;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
EntityManagerInterface $entityManager
|
||||||
|
) {
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Entry $entry): Response
|
||||||
{
|
{
|
||||||
return $this->render('entry/show.html.twig', [
|
return $this->render('entry/show.html.twig', [
|
||||||
'entry' => $entry,
|
'entry' => $entry,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function read(Entry $entry, Request $request): Response
|
||||||
|
{
|
||||||
|
if ($entry->wasRead()) {
|
||||||
|
$this->addFlash('notice', sprintf('Entry "%s" was already marked as read.', $entry->getName()));
|
||||||
|
} else {
|
||||||
|
$entry->markAsRead();
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$this->addFlash('success', sprintf('Entry "%s" was marked as read.', $entry->getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getRedirectResponseAfterModification($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unRead(Entry $entry, Request $request): Response
|
||||||
|
{
|
||||||
|
if ($entry->wasRead()) {
|
||||||
|
$entry->markAsUnRead();
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$this->addFlash('success', sprintf('Entry "%s" was marked as un read.', $entry->getName()));
|
||||||
|
} else {
|
||||||
|
$this->addFlash('notice', sprintf('Entry "%s" was not yet marked as read.', $entry->getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getRedirectResponseAfterModification($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getRedirectResponseAfterModification(Request $request): Response
|
||||||
|
{
|
||||||
|
$redirectTarget = $request->headers->get('referer');
|
||||||
|
if (is_string($redirectTarget)) {
|
||||||
|
return $this->redirect($redirectTarget, 307);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('start', [], 307);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ namespace App\Entity;
|
||||||
use App\Repository\BucketRepository;
|
use App\Repository\BucketRepository;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\Common\Collections\Criteria;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,9 +60,6 @@ class Bucket
|
||||||
return $this->slug;
|
return $this->slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Collection|Feed[]
|
|
||||||
*/
|
|
||||||
public function getFeeds(): Collection
|
public function getFeeds(): Collection
|
||||||
{
|
{
|
||||||
return $this->feeds;
|
return $this->feeds;
|
||||||
|
|
|
@ -128,9 +128,18 @@ class Entry
|
||||||
return $this->content;
|
return $this->content;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Rename into is / was ?
|
public function wasRead(): bool
|
||||||
public function getRead(): bool
|
|
||||||
{
|
{
|
||||||
return $this->read;
|
return $this->read;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function markAsRead(): void
|
||||||
|
{
|
||||||
|
$this->read = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markAsUnRead(): void
|
||||||
|
{
|
||||||
|
$this->read = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ namespace App\Entity;
|
||||||
use App\Repository\FeedRepository;
|
use App\Repository\FeedRepository;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\Common\Collections\Criteria;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,4 +101,11 @@ class Feed
|
||||||
{
|
{
|
||||||
return $this->entries;
|
return $this->entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getUnreadEntries(): Collection
|
||||||
|
{
|
||||||
|
$criteria = Criteria::create();
|
||||||
|
$criteria->where(Criteria::expr()->eq('read', false));
|
||||||
|
return $this->entries->matching($criteria);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,16 @@
|
||||||
<h1><a href="{{ path('start') }}">{{ app_name }}</a></h1>
|
<h1><a href="{{ path('start') }}">{{ app_name }}</a></h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<aside>
|
||||||
|
{% for label, messages in app.flashes %}
|
||||||
|
<ul class="{{ label }}">
|
||||||
|
{% for message in messages %}
|
||||||
|
<li>{{ message }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endfor %}
|
||||||
|
</aside>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{# TODO: collect newest entries of all feeds in controller #}
|
{# TODO: collect newest entries of all feeds in controller #}
|
||||||
{{ include('feed/entries.html.twig', {entries: bucket.feeds.first.entries | slice(0, 10) }) }}
|
{{ include('feed/entries.html.twig', {entries: bucket.feeds.first.unreadEntries | slice(0, 10) }) }}
|
||||||
|
|
||||||
<aside>
|
<aside>
|
||||||
<h1>Feeds in this bucket</h1>
|
<h1>Feeds in this bucket</h1>
|
||||||
|
|
|
@ -28,5 +28,11 @@
|
||||||
|
|
||||||
<a href="{{ path('bucket', {slug: entry.feed.bucket.slug}) }}">{{ entry.feed.bucket.name }} bucket</a>
|
<a href="{{ path('bucket', {slug: entry.feed.bucket.slug}) }}">{{ entry.feed.bucket.name }} bucket</a>
|
||||||
<a href="{{ path('feed', {slug: entry.feed.slug}) }}">{{ entry.feed.name }} feed</a>
|
<a href="{{ path('feed', {slug: entry.feed.slug}) }}">{{ entry.feed.name }} feed</a>
|
||||||
|
|
||||||
|
{% if entry.wasRead %}
|
||||||
|
<a href="{{ path('entry-mark-as-un-read', {slug: entry.slug}) }}">Mark as unread</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ path('entry-mark-as-read', {slug: entry.slug}) }}">Mark as read</a>
|
||||||
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
{% for entry in entries %}
|
{% if entries %}
|
||||||
|
{% for entry in entries %}
|
||||||
{{ include('entry/_single.html.twig', {entry: entry, short: true}) }}
|
{{ include('entry/_single.html.twig', {entry: entry, short: true}) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
No unread entries in this feed.
|
||||||
|
{% endif %}
|
||||||
|
|
|
@ -21,6 +21,6 @@
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{{ include('feed/entries.html.twig', {entries: feed.entries}) }}
|
{{ include('feed/entries.html.twig', {entries: feed.unreadEntries}) }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
Loading…
Reference in a new issue