blog posts
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
"doctrine/doctrine-migrations-bundle": "^3.4",
|
"doctrine/doctrine-migrations-bundle": "^3.4",
|
||||||
"doctrine/orm": "^3.3",
|
"doctrine/orm": "^3.3",
|
||||||
"league/commonmark": "^2.7",
|
"league/commonmark": "^2.7",
|
||||||
|
"league/html-to-markdown": "^5.1",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
"phpstan/phpdoc-parser": "^2.1",
|
"phpstan/phpdoc-parser": "^2.1",
|
||||||
"symfony/asset": "7.2.*",
|
"symfony/asset": "7.2.*",
|
||||||
|
|||||||
91
composer.lock
generated
91
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "c3cbc37c53092d5fb23e7a7e21304c25",
|
"content-hash": "194a6d80a9896ce73994dc1b6c059f1d",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "composer/semver",
|
"name": "composer/semver",
|
||||||
@@ -1635,6 +1635,95 @@
|
|||||||
],
|
],
|
||||||
"time": "2022-12-11T20:36:23+00:00"
|
"time": "2022-12-11T20:36:23+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "league/html-to-markdown",
|
||||||
|
"version": "5.1.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/thephpleague/html-to-markdown.git",
|
||||||
|
"reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/0b4066eede55c48f38bcee4fb8f0aa85654390fd",
|
||||||
|
"reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-xml": "*",
|
||||||
|
"php": "^7.2.5 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mikehaertl/php-shellcommand": "^1.1.0",
|
||||||
|
"phpstan/phpstan": "^1.8.8",
|
||||||
|
"phpunit/phpunit": "^8.5 || ^9.2",
|
||||||
|
"scrutinizer/ocular": "^1.6",
|
||||||
|
"unleashedtech/php-coding-standard": "^2.7 || ^3.0",
|
||||||
|
"vimeo/psalm": "^4.22 || ^5.0"
|
||||||
|
},
|
||||||
|
"bin": [
|
||||||
|
"bin/html-to-markdown"
|
||||||
|
],
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "5.2-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"League\\HTMLToMarkdown\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Colin O'Dell",
|
||||||
|
"email": "colinodell@gmail.com",
|
||||||
|
"homepage": "https://www.colinodell.com",
|
||||||
|
"role": "Lead Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nick Cernis",
|
||||||
|
"email": "nick@cern.is",
|
||||||
|
"homepage": "http://modernnerd.net",
|
||||||
|
"role": "Original Author"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "An HTML-to-markdown conversion helper for PHP",
|
||||||
|
"homepage": "https://github.com/thephpleague/html-to-markdown",
|
||||||
|
"keywords": [
|
||||||
|
"html",
|
||||||
|
"markdown"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/thephpleague/html-to-markdown/issues",
|
||||||
|
"source": "https://github.com/thephpleague/html-to-markdown/tree/5.1.1"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://www.colinodell.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.paypal.me/colinpodell/10.00",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/colinodell",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/league/html-to-markdown",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2023-07-12T21:21:09+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "monolog/monolog",
|
"name": "monolog/monolog",
|
||||||
"version": "3.9.0",
|
"version": "3.9.0",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ services:
|
|||||||
exclude:
|
exclude:
|
||||||
- '../src/DependencyInjection/'
|
- '../src/DependencyInjection/'
|
||||||
- '../src/Entity/'
|
- '../src/Entity/'
|
||||||
|
- '../src/Interface/'
|
||||||
- '../src/Kernel.php'
|
- '../src/Kernel.php'
|
||||||
|
|
||||||
# add more service definitions when explicit configuration is needed
|
# add more service definitions when explicit configuration is needed
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace DoctrineMigrations;
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
use Doctrine\Migrations\AbstractMigration;
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
@@ -38,17 +39,21 @@ final class Version20250522212300 extends AbstractMigration
|
|||||||
$blogPostPath = 'scripts/blog-migrated/' . $slug . '.yaml';
|
$blogPostPath = 'scripts/blog-migrated/' . $slug . '.yaml';
|
||||||
$blogPost = Yaml::parseFile($blogPostPath);
|
$blogPost = Yaml::parseFile($blogPostPath);
|
||||||
|
|
||||||
$publishedDate = $blogPost['pubDate'];
|
$publishedDateTime = DateTime::createFromFormat('U', strval($blogPost['pubDate']));
|
||||||
$updatedDate = array_key_exists(array: $blogPost, key: 'updatedDate')
|
$publishedDate = $publishedDateTime->format('Y-m-d');
|
||||||
? $blogPost['updatedDate']
|
$updatedDateTime = array_key_exists(array: $blogPost, key: 'updatedDate')
|
||||||
|
? DateTime::createFromFormat('U', strval($blogPost['updatedDate']))
|
||||||
: null;
|
: null;
|
||||||
|
$updatedDate = $updatedDateTime == null
|
||||||
|
? 'NULL'
|
||||||
|
: "'" . $updatedDateTime->format('Y-m-d') . "'";
|
||||||
$title = str_replace("'", "''", $blogPost['title']);
|
$title = str_replace("'", "''", $blogPost['title']);
|
||||||
$description = str_replace("'", "''", $blogPost['description']);
|
$description = str_replace("'", "''", $blogPost['description']);
|
||||||
$content = str_replace("'", "''", $blogPost['content']);
|
$content = str_replace("'", "''", $blogPost['content']);
|
||||||
|
|
||||||
$this->addSql(<<<SQL
|
$this->addSql(<<<SQL
|
||||||
INSERT INTO blog_post(slug, published_date, updated_date, title, description, content)
|
INSERT INTO blog_post(slug, published_date, updated_date, title, description, content)
|
||||||
VALUES('$slug', '$publishedDate', '$updatedDate', '$title', '$description', '$content')
|
VALUES('$slug', '$publishedDate', $updatedDate, '$title', '$description', '$content')
|
||||||
SQL);
|
SQL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
--colour-primary-fg: var(--colour-primary-90);
|
--colour-primary-fg: var(--colour-primary-90);
|
||||||
--colour-primary-fg-accent: var(--colour-primary-80);
|
--colour-primary-fg-accent: var(--colour-primary-80);
|
||||||
--colour-primary-bg: var(--colour-primary-10);
|
--colour-primary-bg: var(--colour-primary-10);
|
||||||
--colour-primary-fg-accent: var(--colour-primary-20);
|
--colour-primary-bg-accent: var(--colour-primary-20);
|
||||||
--colour-code-fg: var(--colour-primary-90);
|
--colour-code-fg: var(--colour-primary-90);
|
||||||
--colour-code-bg: var(--colour-primary-15);
|
--colour-code-bg: var(--colour-primary-15);
|
||||||
--colour-hyperlink: var(--colour-hyperlink-80);
|
--colour-hyperlink: var(--colour-hyperlink-80);
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
--colour-primary-fg: var(--colour-primary-20);
|
--colour-primary-fg: var(--colour-primary-20);
|
||||||
--colour-primary-fg-accent: var(--colour-primary-40);
|
--colour-primary-fg-accent: var(--colour-primary-40);
|
||||||
--colour-primary-bg: var(--colour-primary-95);
|
--colour-primary-bg: var(--colour-primary-95);
|
||||||
--colour-primary-fg-accent: var(--colour-primary-90);
|
--colour-primary-bg-accent: var(--colour-primary-90);
|
||||||
--colour-hyperlink: var(--colour-hyperlink-40);
|
--colour-hyperlink: var(--colour-hyperlink-40);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,13 +162,13 @@ h3, h4, h5, h6 {
|
|||||||
|
|
||||||
/** Hyperlinks */
|
/** Hyperlinks */
|
||||||
|
|
||||||
a:is(:link, :visited) {
|
:is(:link, :visited) {
|
||||||
color: var(--colour-hyperlink);
|
color: var(--colour-hyperlink);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
:hover {
|
||||||
text-decoration: wavy;
|
text-decoration: wavy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
p:has(.dt-published, .dt-updated) {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-style: italic;
|
||||||
|
margin-block-start: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.p-summary {
|
.p-summary {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
@@ -1,23 +1,76 @@
|
|||||||
.skip-to {
|
h2 {
|
||||||
display: inline-block;
|
margin-block-start: var(--spacing-block-md);
|
||||||
margin-block-start: var(--spacing-block-xs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.skip-to ul {
|
h3 {
|
||||||
margin-block-start: var(--spacing-block-xs);
|
margin-block-start: var(--spacing-block-sm);
|
||||||
}
|
|
||||||
|
|
||||||
.skip-to :is(ul, li) {
|
|
||||||
display: inline-block;
|
|
||||||
list-style-type: disc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skip-to li:last-of-type {
|
|
||||||
list-style: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-entry {
|
.h-entry {
|
||||||
outline: 0.125rem solid var(--colour-primary-fg);
|
background-color: var(--colour-primary-bg-accent);
|
||||||
outline-offset: 1rem;
|
border: 0.125rem solid var(--colour-primary-fg);
|
||||||
border-radius: 0.25rem;
|
border-radius: 1rem;
|
||||||
|
margin-inline: -1rem;
|
||||||
|
margin-block-start: var(--spacing-block-sm);
|
||||||
|
transition-duration: 250ms;
|
||||||
|
|
||||||
|
&:is(:hover, :focus, :focus-within, :focus-visible) {
|
||||||
|
background-color: var(--colour-primary-fg-accent);
|
||||||
|
color: var(--colour-primary-bg-accent);
|
||||||
|
transition-duration: 100ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ .h-entry {
|
||||||
|
margin-block-start: var(--spacing-block-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.u-url {
|
||||||
|
color: inherit;
|
||||||
|
display: inline-block;
|
||||||
|
height: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
text-decoration: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-name {
|
||||||
|
margin-block-start: 0;
|
||||||
|
|
||||||
|
+ * {
|
||||||
|
margin-block-start: var(--spacing-block-xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p:has(.dt-published, .dt-updated) {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-style: italic;
|
||||||
|
|
||||||
|
&:not(.p-name + *) {
|
||||||
|
margin-block-start: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-to {
|
||||||
|
display: inline-block;
|
||||||
|
margin-block-start: var(--spacing-block-xs);
|
||||||
|
|
||||||
|
:is(ul, li) {
|
||||||
|
display: inline-block;
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin-block-start: var(--spacing-block-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
li:last-of-type {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.h-entry {
|
||||||
|
transition-property: background-color, color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\BlogPost;
|
||||||
use App\Entity\Note;
|
use App\Entity\Note;
|
||||||
use App\Form\Type\NoteType;
|
use App\Form\Type\NoteType;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
@@ -14,7 +15,25 @@ use Symfony\Component\Routing\Annotation\Route;
|
|||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
use function Symfony\Component\HttpFoundation\Response;
|
use function Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
const MONTH_NAMES = [
|
||||||
|
'01' => 'January',
|
||||||
|
'02' => 'February',
|
||||||
|
'03' => 'March',
|
||||||
|
'04' => 'April',
|
||||||
|
'05' => 'May',
|
||||||
|
'06' => 'June',
|
||||||
|
'07' => 'July',
|
||||||
|
'08' => 'August',
|
||||||
|
'09' => 'September',
|
||||||
|
'10' => 'October',
|
||||||
|
'11' => 'November',
|
||||||
|
'12' => 'December',
|
||||||
|
];
|
||||||
|
|
||||||
class GuiController extends AbstractController {
|
class GuiController extends AbstractController {
|
||||||
|
/**
|
||||||
|
* @return array<string,string>
|
||||||
|
*/
|
||||||
#[Route('/', name: 'index')]
|
#[Route('/', name: 'index')]
|
||||||
#[Template('/index.html.twig')]
|
#[Template('/index.html.twig')]
|
||||||
public function index(): array {
|
public function index(): array {
|
||||||
@@ -23,7 +42,9 @@ class GuiController extends AbstractController {
|
|||||||
'description' => 'Joe Carstairs\' personal website',
|
'description' => 'Joe Carstairs\' personal website',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
#[Route('/notes', name: 'notes')]
|
#[Route('/notes', name: 'notes')]
|
||||||
#[Template('/notes.html.twig')]
|
#[Template('/notes.html.twig')]
|
||||||
public function notes(
|
public function notes(
|
||||||
@@ -43,93 +64,115 @@ class GuiController extends AbstractController {
|
|||||||
return [
|
return [
|
||||||
'title' => 'Joe Carstairs\' notes',
|
'title' => 'Joe Carstairs\' notes',
|
||||||
'description' => 'Joe Carstairs\' notes',
|
'description' => 'Joe Carstairs\' notes',
|
||||||
|
'isFeed' => True,
|
||||||
'notes' => $notesByYearAndMonth,
|
'notes' => $notesByYearAndMonth,
|
||||||
'years' => $years,
|
'years' => $years,
|
||||||
'months' => $monthsByYear,
|
'months' => $monthsByYear,
|
||||||
'monthNames' => [
|
'monthNames' => MONTH_NAMES,
|
||||||
'01' => 'January',
|
];
|
||||||
'02' => 'February',
|
}
|
||||||
'03' => 'March',
|
/**
|
||||||
'04' => 'April',
|
* @return array<string,mixed>
|
||||||
'05' => 'May',
|
*/
|
||||||
'06' => 'June',
|
#[Route('/blog', name: 'blog_posts')]
|
||||||
'07' => 'July',
|
#[Template('/blog_posts.html.twig')]
|
||||||
'08' => 'August',
|
public function blogPosts(
|
||||||
'09' => 'September',
|
EntityManagerInterface $entityManager,
|
||||||
'10' => 'October',
|
): array {
|
||||||
'11' => 'November',
|
$posts = $entityManager->getRepository(BlogPost::class)->findAllOrderedBySlugDesc();
|
||||||
'12' => 'December',
|
$years = $this->getYears($posts);
|
||||||
],
|
$postsByYear = $this->groupByYear($posts);
|
||||||
|
$months = array_map(array: $years, callback: fn($year) => $this->getMonths($postsByYear[$year]));
|
||||||
|
$monthsByYear = array_combine(keys: $years, values: $months);
|
||||||
|
$groupByMonth = fn($posts) => $this->groupByMonth($posts);
|
||||||
|
$postsByYearAndMonth = array_map(
|
||||||
|
array: $postsByYear,
|
||||||
|
callback: $groupByMonth,
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => 'Joe Carstairs\' blog',
|
||||||
|
'description' => 'Joe Carstairs\' blog',
|
||||||
|
'isFeed' => True,
|
||||||
|
'posts' => $postsByYearAndMonth,
|
||||||
|
'years' => $years,
|
||||||
|
'months' => $monthsByYear,
|
||||||
|
'monthNames' => MONTH_NAMES,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $notes Note[]
|
* @@template T of \FeedEntry
|
||||||
* @return Note[][]
|
* @param $entries T[]
|
||||||
|
* @return T[][]
|
||||||
*/
|
*/
|
||||||
function groupByYear(array $notes): array {
|
function groupByYear(array $entries): array {
|
||||||
$years = $this->getYears($notes);
|
$years = $this->getYears($entries);
|
||||||
$filterByYear = fn(string $year) =>
|
$filterByYear = fn(string $year) =>
|
||||||
fn($note) => ($note->getPublishedDate()->format('Y') == $year);
|
fn($entry) => ($entry->getPublishedDate()->format('Y') == $year);
|
||||||
$notesForYear = fn(string $year) =>
|
$entriesForYear = fn(string $year) =>
|
||||||
array_filter(array: $notes, callback: $filterByYear($year));
|
array_filter(array: $entries, callback: $filterByYear($year));
|
||||||
$notesByYear = array_map(
|
$entriesByYear = array_map(
|
||||||
array: $years,
|
array: $years,
|
||||||
callback: $notesForYear,
|
callback: $entriesForYear,
|
||||||
);
|
);
|
||||||
return array_combine(keys: $years, values: $notesByYear);
|
return array_combine(keys: $years, values: $entriesByYear);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $notes Note[]
|
* @template T of \FeedEntry
|
||||||
* @return string[]
|
* @param $entries T[]
|
||||||
*/
|
* @return string[]
|
||||||
function getYears(array $notes): array {
|
*/
|
||||||
$getYear = fn(Note $note): string => $note->getPublishedDate()->format('Y');
|
function getYears(array $entries): array {
|
||||||
$years = array_map(array: $notes, callback: $getYear);
|
$getYear = fn($note): string => $note->getPublishedDate()->format('Y');
|
||||||
|
$years = array_map(array: $entries, callback: $getYear);
|
||||||
$years = array_unique($years);
|
$years = array_unique($years);
|
||||||
arsort($years);
|
arsort($years);
|
||||||
return $years;
|
return $years;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $notes Note[]
|
* @template T of \FeedEntry
|
||||||
* @return Note[]
|
* @param $entries T[]
|
||||||
|
* @return T[]
|
||||||
*/
|
*/
|
||||||
function groupByMonth(array $notes): array {
|
function groupByMonth(array $entries): array {
|
||||||
$months = $this->getMonths($notes);
|
$months = $this->getMonths($entries);
|
||||||
$filterByMonth = fn(string $month) =>
|
$filterByMonth = fn(string $month) =>
|
||||||
fn($note) => ($note->getPublishedDate()->format('m') == $month);
|
fn($entry) => ($entry->getPublishedDate()->format('m') == $month);
|
||||||
$notesByMonth = array_map(
|
$entriesByMonth = array_map(
|
||||||
array: $months,
|
array: $months,
|
||||||
callback: fn(string $month) =>
|
callback: fn(string $month) =>
|
||||||
array_filter(array: $notes, callback: $filterByMonth($month)),
|
array_filter(array: $entries, callback: $filterByMonth($month)),
|
||||||
);
|
);
|
||||||
return array_combine(keys: $months, values: $notesByMonth);
|
return array_combine(keys: $months, values: $entriesByMonth);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $notes Note[]
|
* @@template T of \FeedEntry
|
||||||
* @return string[]
|
* @param $entries T[]
|
||||||
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
function getMonths(array $notes): array {
|
function getMonths(array $entries): array {
|
||||||
$getMonth = fn(Note $note): string => $note->getPublishedDate()->format('m');
|
$getMonth = fn($entry) => $entry->getPublishedDate()->format('m');
|
||||||
$months = array_map(array: $notes, callback: $getMonth);
|
$months = array_map(array: $entries, callback: $getMonth);
|
||||||
$months = array_unique($months);
|
$months = array_unique($months);
|
||||||
arsort($months);
|
arsort($months);
|
||||||
return $months;
|
return $months;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $notes Note[]
|
* @template T of \FeedEntry
|
||||||
* @return Note[]
|
* @param $entries T[]
|
||||||
|
* @return T[]
|
||||||
*/
|
*/
|
||||||
function sortBySlug(array $notes): array {
|
function sortBySlug(array $entries): array {
|
||||||
$getSlug = fn(Note $note): string => $note->getSlug();
|
$getSlug = fn($entry): string => $entry->getSlug();
|
||||||
$slugs = array_map(array: $notes, callback: $getSlug);
|
$slugs = array_map(array: $entries, callback: $getSlug);
|
||||||
$notesBySlug = array_combine(keys: $slugs, values: $notes);
|
$entriesBySlug = array_combine(keys: $slugs, values: $entries);
|
||||||
krsort($notesBySlug);
|
krsort($entriesBySlug);
|
||||||
return array_values($notesBySlug);
|
return array_values($entriesBySlug);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[IsGranted('ROLE_EDITOR')]
|
#[IsGranted('ROLE_EDITOR')]
|
||||||
@@ -185,8 +228,30 @@ class GuiController extends AbstractController {
|
|||||||
return [
|
return [
|
||||||
'title' => 'Joe Carstairs\' notes',
|
'title' => 'Joe Carstairs\' notes',
|
||||||
'description' => 'Joe Carstairs\' notes',
|
'description' => 'Joe Carstairs\' notes',
|
||||||
|
'isFeedEntry' => True,
|
||||||
'note' => $note,
|
'note' => $note,
|
||||||
'slug' => $slug,
|
'slug' => $slug,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
#[Route('/blog/{slug}', name: 'blog_post')]
|
||||||
|
#[Template('/blog_post.html.twig')]
|
||||||
|
public function blogPost(
|
||||||
|
EntityManagerInterface $entityManager,
|
||||||
|
string $slug,
|
||||||
|
): array {
|
||||||
|
$repository = $entityManager->getRepository(BlogPost::class);
|
||||||
|
$post = $repository->findOneBy(['slug' => $slug]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => $post->getTitle(),
|
||||||
|
'description' => $post->getDescription(),
|
||||||
|
'isFeedEntry' => True,
|
||||||
|
'post' => $post,
|
||||||
|
'slug' => $slug,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Interface\FeedEntry;
|
||||||
use App\Repository\BlogPostRepository;
|
use App\Repository\BlogPostRepository;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: BlogPostRepository::class)]
|
#[ORM\Entity(repositoryClass: BlogPostRepository::class)]
|
||||||
class BlogPost
|
class BlogPost implements FeedEntry
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Interface\FeedEntry;
|
||||||
use App\Repository\NoteRepository;
|
use App\Repository\NoteRepository;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: NoteRepository::class)]
|
#[ORM\Entity(repositoryClass: NoteRepository::class)]
|
||||||
class Note
|
class Note implements FeedEntry
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
|
|||||||
8
src/Interface/FeedEntry.php
Normal file
8
src/Interface/FeedEntry.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Interface;
|
||||||
|
|
||||||
|
interface FeedEntry {
|
||||||
|
function getPublishedDate(): ?\DateTime;
|
||||||
|
function getSlug(): ?string;
|
||||||
|
}
|
||||||
@@ -14,4 +14,15 @@ class BlogPostRepository extends ServiceEntityRepository
|
|||||||
public function __construct(ManagerRegistry $registry) {
|
public function __construct(ManagerRegistry $registry) {
|
||||||
parent::__construct($registry, BlogPost::class);
|
parent::__construct($registry, BlogPost::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return BlogPost[]
|
||||||
|
*/
|
||||||
|
public function findAllOrderedBySlugDesc(): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('n')
|
||||||
|
->orderBy('n.slug', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,10 @@
|
|||||||
<body>
|
<body>
|
||||||
{{ include('components/_navbar.html.twig') }}
|
{{ include('components/_navbar.html.twig') }}
|
||||||
|
|
||||||
<main>
|
{% block main %}
|
||||||
{% block content %}{% endblock %}
|
<main>
|
||||||
</main>
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
47
templates/blog_post.html.twig
Normal file
47
templates/blog_post.html.twig
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if post %}
|
||||||
|
<article class="h-entry">
|
||||||
|
<aside>
|
||||||
|
<span>
|
||||||
|
This is a blog post by
|
||||||
|
<a class="p-author h-card" href="/">Joe Carstairs</a>.
|
||||||
|
</span>
|
||||||
|
<p>
|
||||||
|
Published: <time class="dt-published">{{ post.publishedDate.format('c') }}
|
||||||
|
</p>
|
||||||
|
{% if post.updatedDate %}}
|
||||||
|
<p>
|
||||||
|
Updated: <time class="dt-updated">{{ post.updatedDate.format('c') }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
<span hidden>
|
||||||
|
<a class="u-url uid" href="{{ url('blog_post', { slug: post.slug }) }}">
|
||||||
|
Permalink
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1 class="p-name">{% apply markdown_to_html %}{{ post.title }}{% endapply %}</h1>
|
||||||
|
<p class="p-summary">
|
||||||
|
{% apply markdown_to_html %}{{ post.description }}{% endapply %}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
{% apply markdown_to_html %}
|
||||||
|
<section class="e-content">
|
||||||
|
{{ post.content }}
|
||||||
|
</section>
|
||||||
|
{% endapply %}
|
||||||
|
</article>
|
||||||
|
{% else %}
|
||||||
|
<section>
|
||||||
|
<h1>Post not found</h1>
|
||||||
|
<p>I don't have a blog post '{{ slug }}'.</p>
|
||||||
|
<p>Go back to <a href="{{ path('blog_posts') }}">Blog</a>.</p>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
81
templates/blog_posts.html.twig
Normal file
81
templates/blog_posts.html.twig
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="h-feed">
|
||||||
|
<h1 class="p-name">Joe Carstairs' blog</h1>
|
||||||
|
|
||||||
|
<p hidden>
|
||||||
|
This blog is written by
|
||||||
|
<a class="p-author h-card" href="{{ url('index') }}">
|
||||||
|
Joe Carstairs
|
||||||
|
</a>.
|
||||||
|
</p>
|
||||||
|
<p hidden>
|
||||||
|
<a class="u-url" href="{{ url('blog_posts') }}">Permalink</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if years %}
|
||||||
|
<nav class="skip-to">
|
||||||
|
Skip to:
|
||||||
|
<ul>
|
||||||
|
{% for year in years %}
|
||||||
|
<li><a href="#{{ year }}">{{ year }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for year in years %}
|
||||||
|
<h2 id="{{ year }}">{{ year }}</h2>
|
||||||
|
|
||||||
|
<nav class="skip-to">
|
||||||
|
Skip to:
|
||||||
|
<ul>
|
||||||
|
{% for month in months[year] %}
|
||||||
|
<li><a href="#{{ year }}-{{ month }}">
|
||||||
|
{{ monthNames[month] }} <span class="visually-hidden">{{ year }}</span>
|
||||||
|
</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{% for month in months[year] %}
|
||||||
|
<h3 id="{{ year }}-{{ month }}">{{ monthNames[month] }}</h3>
|
||||||
|
|
||||||
|
{% for post in posts[year][month] %}
|
||||||
|
<section class="h-entry">
|
||||||
|
<a class="u-url" href="{{ path('blog_post', { 'slug': post.slug }) }}">
|
||||||
|
<h4 class="p-name">
|
||||||
|
{{ post.title }}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Added:
|
||||||
|
<time class="dt-published" datetime="{{ post.publishedDate.format('c') }}">
|
||||||
|
{{ post.publishedDate.format('j F Y') }}
|
||||||
|
</time>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if post.updatedDate %}
|
||||||
|
<p>
|
||||||
|
Updated:
|
||||||
|
<time class="dt-updated" datetime="{{ post.updatedDate.format('c') }}">
|
||||||
|
{{ post.updatedDate.format('j F Y') }}
|
||||||
|
</time>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<section class="p-summary">
|
||||||
|
{% apply markdown_to_html %}
|
||||||
|
{{ post.description|markdown_to_html|striptags('<i><em><b><strong><sup><p>')|html_to_markdown }}
|
||||||
|
{% endapply %}
|
||||||
|
</section>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p>I have no blog posts.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/blog">Blog</a>
|
<a href="{{ path('blog_posts') }}">Blog</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('notes') }}">Notes</a>
|
<a href="{{ path('notes') }}">Notes</a>
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
<link rel="stylesheet" href="/css/reset.css" />
|
<link rel="stylesheet" href="/css/reset.css" />
|
||||||
<link rel="stylesheet" href="/css/base.css" />
|
<link rel="stylesheet" href="/css/base.css" />
|
||||||
<link rel="stylesheet" href="/css/hcard.css" />
|
<link rel="stylesheet" href="/css/hcard.css" />
|
||||||
<link rel="stylesheet" href="/css/feed.css" />
|
{% if isFeed|default(false) %}
|
||||||
|
<link rel="stylesheet" href="/css/feed.css" />
|
||||||
|
{% endif %}
|
||||||
|
{% if isFeedEntry|default(false) %}
|
||||||
|
<link rel="stylesheet" href="/css/feed-entry.css" />
|
||||||
|
{% endif %}
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
{% extends 'base.html.twig' %}
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block main %}
|
||||||
{% if note %}
|
{% if note %}
|
||||||
<section class="h-entry">
|
<article class="h-entry">
|
||||||
<h1 class="p-name">Note {{ note.slug }}</h1>
|
<header>
|
||||||
<a hidden class="p-author h-card" href="{{ url('index') }}">Joe Carstairs</a>
|
<h1 class="p-name">Note {{ note.slug }}</h1>
|
||||||
<a hidden class="u-url" href="{{ url('note', { slug: note.slug }) }}">Permalink</a>
|
<p hidden><a class="p-author h-card" href="{{ url('index') }}">Joe Carstairs</a></p>
|
||||||
<time class="dt-published">{{ note.publishedDate.format('c') }}
|
<p hidden <a class="u-url" href="{{ url('note', { slug: note.slug }) }}">Permalink</a></p>
|
||||||
{% apply markdown_to_html %}
|
<p>Added: <time class="dt-published">{{ note.publishedDate.format('c') }}</time></p>
|
||||||
<section class="e-content">
|
</header>
|
||||||
{{ note.content }}
|
|
||||||
</section>
|
<section class="e-content">
|
||||||
{% endapply %}
|
{{ note.content|markdown_to_html }}
|
||||||
</section>
|
</section>
|
||||||
|
</article>
|
||||||
{% else %}
|
{% else %}
|
||||||
<section>
|
<main>
|
||||||
<h1>Note not found</h1>
|
<header>
|
||||||
<p>I don't have a note '{{ slug }}'.</p>
|
<h1>Note not found</h1>
|
||||||
<p>Go back to <a href="/notes">Notes</a>.</p>
|
</header>
|
||||||
</section>
|
<section>
|
||||||
|
<p>I don't have a note '{{ slug }}'.</p>
|
||||||
|
<p>Go back to <a href="/notes">Notes</a>.</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -14,14 +14,16 @@
|
|||||||
<a class="u-url" href="{{ url('notes') }}">Permalink</a>
|
<a class="u-url" href="{{ url('notes') }}">Permalink</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<nav class="skip-to">
|
{% if years %}
|
||||||
Skip to:
|
<nav class="skip-to">
|
||||||
<ul>
|
Skip to:
|
||||||
{% for year in years %}
|
<ul>
|
||||||
<li><a href="#{{ year }}">{{ year }}</a></li>
|
{% for year in years %}
|
||||||
{% endfor %}
|
<li><a href="#{{ year }}">{{ year }}</a></li>
|
||||||
</ul>
|
{% endfor %}
|
||||||
</nav>
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% for year in years %}
|
{% for year in years %}
|
||||||
<h2 id="{{ year }}">{{ year }}</h2>
|
<h2 id="{{ year }}">{{ year }}</h2>
|
||||||
@@ -42,18 +44,23 @@
|
|||||||
|
|
||||||
{% for note in notes[year][month] %}
|
{% for note in notes[year][month] %}
|
||||||
<section class="h-entry">
|
<section class="h-entry">
|
||||||
<h4><a class="p-name u-url" href="{{ url('note', { slug: note.slug }) }}">
|
<a class="u-url" href="{{ url('note', { slug: note.slug }) }}">
|
||||||
Note {{ note.slug }}
|
<h4 class="p-name">
|
||||||
</a></h4>
|
Note {{ note.slug }}
|
||||||
<time class="dt-published" datetime="{{ note.publishedDate.format('c') }}">
|
</h4>
|
||||||
{{ note.publishedDate.format('j F Y') }}
|
<p>
|
||||||
</time>
|
Added:
|
||||||
|
<time class="dt-published" datetime="{{ note.publishedDate.format('c') }}">
|
||||||
|
{{ note.publishedDate.format('j F Y') }}
|
||||||
|
</time>
|
||||||
|
</p>
|
||||||
|
|
||||||
<section class="e-content">
|
<section class="e-content">
|
||||||
{% apply markdown_to_html %}
|
{% apply markdown_to_html %}
|
||||||
{{ note.content }}
|
{{ note.content|markdown_to_html|striptags('<i><em><b><strong><sup><p>')|html_to_markdown }}
|
||||||
{% endapply %}
|
{% endapply %}
|
||||||
</section>
|
</section>
|
||||||
|
</a>
|
||||||
</section>
|
</section>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
2
todo.txt
2
todo.txt
@@ -1,5 +1,3 @@
|
|||||||
Add blog post page
|
|
||||||
Add blog index page
|
|
||||||
Add a most recent blog post to my home page
|
Add a most recent blog post to my home page
|
||||||
Add a most recent note to my home page
|
Add a most recent note to my home page
|
||||||
Write a script to take a database dump
|
Write a script to take a database dump
|
||||||
|
|||||||
Reference in New Issue
Block a user