Move symfony stuff to own folder
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# In all environments, the following files are loaded if they exist,
|
||||
# the latter taking precedence over the former:
|
||||
#
|
||||
# * .env contains default values for the environment variables needed by the app
|
||||
# * .env.local uncommitted file with local overrides
|
||||
# * .env.$APP_ENV committed environment-specific defaults
|
||||
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
||||
#
|
||||
# Real environment variables win over .env files.
|
||||
#
|
||||
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
||||
# https://symfony.com/doc/current/configuration/secrets.html
|
||||
#
|
||||
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
||||
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=dev
|
||||
APP_SECRET=
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
||||
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||
#
|
||||
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
|
||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
||||
# DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
###> symfony/messenger ###
|
||||
# Choose one of the transports below
|
||||
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
||||
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
|
||||
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
||||
###< symfony/messenger ###
|
||||
|
||||
###> symfony/mailer ###
|
||||
MAILER_DSN=null://null
|
||||
###< symfony/mailer ###
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_SECRET=59e8f6a1ec4b7c0d4c3126975ed96a64
|
||||
###< symfony/framework-bundle ###
|
||||
@@ -0,0 +1,6 @@
|
||||
# define your env variables for the test env here
|
||||
KERNEL_CLASS='App\Kernel'
|
||||
APP_SECRET='$ecretf0rt3st'
|
||||
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||
PANTHER_APP_ENV=panther
|
||||
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
|
||||
@@ -0,0 +1,24 @@
|
||||
###> symfony/framework-bundle ###
|
||||
.env.local
|
||||
.env.local.php
|
||||
.env.*.local
|
||||
config/secrets/prod/prod.decrypt.private.php
|
||||
public/bundles/
|
||||
var/
|
||||
vendor/
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> phpunit/phpunit ###
|
||||
phpunit.xml
|
||||
.phpunit.result.cache
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
###> symfony/phpunit-bridge ###
|
||||
.phpunit.result.cache
|
||||
phpunit.xml
|
||||
###< symfony/phpunit-bridge ###
|
||||
|
||||
###> symfony/asset-mapper ###
|
||||
public/assets/
|
||||
assets/vendor/
|
||||
###< symfony/asset-mapper ###
|
||||
@@ -0,0 +1,10 @@
|
||||
import './bootstrap.js';
|
||||
/*
|
||||
* Welcome to your app's main JavaScript file!
|
||||
*
|
||||
* This file will be included onto the page via the importmap() Twig function,
|
||||
* which should already be in your base.html.twig.
|
||||
*/
|
||||
import './styles/app.css';
|
||||
|
||||
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import { startStimulusApp } from '@symfony/stimulus-bundle';
|
||||
|
||||
const app = startStimulusApp();
|
||||
// register any custom, 3rd party controllers here
|
||||
// app.register('some_controller_name', SomeImportedController);
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"controllers": {
|
||||
"@symfony/ux-turbo": {
|
||||
"turbo-core": {
|
||||
"enabled": true,
|
||||
"fetch": "eager"
|
||||
},
|
||||
"mercure-turbo-stream": {
|
||||
"enabled": false,
|
||||
"fetch": "eager"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoints": []
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
|
||||
const tokenCheck = /^[-_\/+a-zA-Z0-9]{24,}$/;
|
||||
|
||||
// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
|
||||
document.addEventListener('submit', function (event) {
|
||||
generateCsrfToken(event.target);
|
||||
}, true);
|
||||
|
||||
// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
|
||||
// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
|
||||
document.addEventListener('turbo:submit-start', function (event) {
|
||||
const h = generateCsrfHeaders(event.detail.formSubmission.formElement);
|
||||
Object.keys(h).map(function (k) {
|
||||
event.detail.formSubmission.fetchRequest.headers[k] = h[k];
|
||||
});
|
||||
});
|
||||
|
||||
// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
|
||||
document.addEventListener('turbo:submit-end', function (event) {
|
||||
removeCsrfToken(event.detail.formSubmission.formElement);
|
||||
});
|
||||
|
||||
export function generateCsrfToken (formElement) {
|
||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return;
|
||||
}
|
||||
|
||||
let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
let csrfToken = csrfField.value;
|
||||
|
||||
if (!csrfCookie && nameCheck.test(csrfToken)) {
|
||||
csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
|
||||
csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
|
||||
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
if (csrfCookie && tokenCheck.test(csrfToken)) {
|
||||
const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
|
||||
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
|
||||
}
|
||||
}
|
||||
|
||||
export function generateCsrfHeaders (formElement) {
|
||||
const headers = {};
|
||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
|
||||
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
|
||||
headers[csrfCookie] = csrfField.value;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
export function removeCsrfToken (formElement) {
|
||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return;
|
||||
}
|
||||
|
||||
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
|
||||
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
|
||||
const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0';
|
||||
|
||||
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
|
||||
}
|
||||
}
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default 'csrf-protection-controller';
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/*
|
||||
* This is an example Stimulus controller!
|
||||
*
|
||||
* Any element with a data-controller="hello" attribute will cause
|
||||
* this controller to be executed. The name "hello" comes from the filename:
|
||||
* hello_controller.js -> "hello"
|
||||
*
|
||||
* Delete this file or adapt it for your use!
|
||||
*/
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
background-color: skyblue;
|
||||
}
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
|
||||
if (!is_dir(dirname(__DIR__).'/vendor')) {
|
||||
throw new LogicException('Dependencies are missing. Try running "composer install".');
|
||||
}
|
||||
|
||||
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
return function (array $context) {
|
||||
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
|
||||
return new Application($kernel);
|
||||
};
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
if (!ini_get('date.timezone')) {
|
||||
ini_set('date.timezone', 'UTC');
|
||||
}
|
||||
|
||||
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
|
||||
if (PHP_VERSION_ID >= 80000) {
|
||||
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
|
||||
} else {
|
||||
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
|
||||
require PHPUNIT_COMPOSER_INSTALL;
|
||||
PHPUnit\TextUI\Command::main();
|
||||
}
|
||||
} else {
|
||||
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
|
||||
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"type": "project",
|
||||
"license": "proprietary",
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"doctrine/dbal": "^3",
|
||||
"doctrine/doctrine-bundle": "^2.14",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.4",
|
||||
"doctrine/orm": "^3.3",
|
||||
"league/commonmark": "^2.7",
|
||||
"league/html-to-markdown": "^5.1",
|
||||
"phpdocumentor/reflection-docblock": "^5.6",
|
||||
"phpstan/phpdoc-parser": "^2.1",
|
||||
"symfony/asset": "7.2.*",
|
||||
"symfony/asset-mapper": "7.2.*",
|
||||
"symfony/console": "7.2.*",
|
||||
"symfony/doctrine-messenger": "7.2.*",
|
||||
"symfony/dotenv": "7.2.*",
|
||||
"symfony/expression-language": "7.2.*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/form": "7.2.*",
|
||||
"symfony/framework-bundle": "7.2.*",
|
||||
"symfony/http-client": "7.2.*",
|
||||
"symfony/intl": "7.2.*",
|
||||
"symfony/mailer": "7.2.*",
|
||||
"symfony/mime": "7.2.*",
|
||||
"symfony/monolog-bundle": "^3.0",
|
||||
"symfony/notifier": "7.2.*",
|
||||
"symfony/process": "7.2.*",
|
||||
"symfony/property-access": "7.2.*",
|
||||
"symfony/property-info": "7.2.*",
|
||||
"symfony/runtime": "7.2.*",
|
||||
"symfony/security-bundle": "7.2.*",
|
||||
"symfony/serializer": "7.2.*",
|
||||
"symfony/stimulus-bundle": "^2.24",
|
||||
"symfony/string": "7.2.*",
|
||||
"symfony/translation": "7.2.*",
|
||||
"symfony/twig-bundle": "7.2.*",
|
||||
"symfony/ux-turbo": "^2.24",
|
||||
"symfony/validator": "7.2.*",
|
||||
"symfony/web-link": "7.2.*",
|
||||
"symfony/yaml": "7.2.*",
|
||||
"twig/extra-bundle": "^2.12|^3.0",
|
||||
"twig/markdown-extra": "^3.21",
|
||||
"twig/twig": "^2.12|^3.0"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true,
|
||||
"symfony/flex": true,
|
||||
"symfony/runtime": true
|
||||
},
|
||||
"bump-after-update": true,
|
||||
"sort-packages": true
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"App\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"replace": {
|
||||
"symfony/polyfill-ctype": "*",
|
||||
"symfony/polyfill-iconv": "*",
|
||||
"symfony/polyfill-php72": "*",
|
||||
"symfony/polyfill-php73": "*",
|
||||
"symfony/polyfill-php74": "*",
|
||||
"symfony/polyfill-php80": "*",
|
||||
"symfony/polyfill-php81": "*",
|
||||
"symfony/polyfill-php82": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"auto-scripts": {
|
||||
"cache:clear": "symfony-cmd",
|
||||
"assets:install %PUBLIC_DIR%": "symfony-cmd",
|
||||
"importmap:install": "symfony-cmd"
|
||||
},
|
||||
"post-install-cmd": [
|
||||
"@auto-scripts"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@auto-scripts"
|
||||
]
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
},
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "7.2.*"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"symfony/browser-kit": "7.2.*",
|
||||
"symfony/css-selector": "7.2.*",
|
||||
"symfony/debug-bundle": "7.2.*",
|
||||
"symfony/maker-bundle": "^1.63",
|
||||
"symfony/phpunit-bridge": "^7.2",
|
||||
"symfony/stopwatch": "7.2.*",
|
||||
"symfony/web-profiler-bundle": "7.2.*"
|
||||
}
|
||||
}
|
||||
Generated
+10506
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
|
||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
|
||||
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
|
||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||
];
|
||||
@@ -0,0 +1,11 @@
|
||||
framework:
|
||||
asset_mapper:
|
||||
# The paths to make available to the asset mapper.
|
||||
paths:
|
||||
- assets/
|
||||
missing_import_mode: strict
|
||||
|
||||
when@prod:
|
||||
framework:
|
||||
asset_mapper:
|
||||
missing_import_mode: warn
|
||||
@@ -0,0 +1,19 @@
|
||||
framework:
|
||||
cache:
|
||||
# Unique name of your app: used to compute stable namespaces for cache keys.
|
||||
#prefix_seed: your_vendor_name/app_name
|
||||
|
||||
# The "app" cache stores to the filesystem by default.
|
||||
# The data in this cache should persist between deploys.
|
||||
# Other options include:
|
||||
|
||||
# Redis
|
||||
#app: cache.adapter.redis
|
||||
#default_redis_provider: redis://localhost
|
||||
|
||||
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
|
||||
#app: cache.adapter.apcu
|
||||
|
||||
# Namespaced pools use the above "app" backend by default
|
||||
#pools:
|
||||
#my.dedicated.cache: null
|
||||
@@ -0,0 +1,11 @@
|
||||
# Enable stateless CSRF protection for forms and logins/logouts
|
||||
framework:
|
||||
form:
|
||||
csrf_protection:
|
||||
token_id: submit
|
||||
|
||||
csrf_protection:
|
||||
stateless_token_ids:
|
||||
- submit
|
||||
- authenticate
|
||||
- logout
|
||||
@@ -0,0 +1,5 @@
|
||||
when@dev:
|
||||
debug:
|
||||
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
|
||||
# See the "server:dump" command to start a new server.
|
||||
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"
|
||||
@@ -0,0 +1,54 @@
|
||||
doctrine:
|
||||
dbal:
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
|
||||
# IMPORTANT: You MUST configure your server version,
|
||||
# either here or in the DATABASE_URL env var (see .env file)
|
||||
#server_version: '16'
|
||||
|
||||
profiling_collect_backtrace: '%kernel.debug%'
|
||||
use_savepoints: true
|
||||
orm:
|
||||
auto_generate_proxy_classes: true
|
||||
enable_lazy_ghost_objects: true
|
||||
report_fields_where_declared: true
|
||||
validate_xml_mapping: true
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
identity_generation_preferences:
|
||||
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
||||
auto_mapping: true
|
||||
mappings:
|
||||
App:
|
||||
type: attribute
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: App
|
||||
controller_resolver:
|
||||
auto_mapping: false
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
dbal:
|
||||
# "TEST_TOKEN" is typically set by ParaTest
|
||||
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
|
||||
|
||||
when@prod:
|
||||
doctrine:
|
||||
orm:
|
||||
auto_generate_proxy_classes: false
|
||||
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
|
||||
query_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
result_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.result_cache_pool
|
||||
|
||||
framework:
|
||||
cache:
|
||||
pools:
|
||||
doctrine.result_cache_pool:
|
||||
adapter: cache.app
|
||||
doctrine.system_cache_pool:
|
||||
adapter: cache.system
|
||||
@@ -0,0 +1,6 @@
|
||||
doctrine_migrations:
|
||||
migrations_paths:
|
||||
# namespace is arbitrary but should be different from App\Migrations
|
||||
# as migrations classes should NOT be autoloaded
|
||||
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||
enable_profiler: false
|
||||
@@ -0,0 +1,15 @@
|
||||
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
|
||||
# Note that the session will be started ONLY if you read or write from it.
|
||||
session: true
|
||||
|
||||
#esi: true
|
||||
#fragments: true
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
test: true
|
||||
session:
|
||||
storage_factory_id: session.storage.factory.mock_file
|
||||
@@ -0,0 +1,3 @@
|
||||
framework:
|
||||
mailer:
|
||||
dsn: '%env(MAILER_DSN)%'
|
||||
@@ -0,0 +1,29 @@
|
||||
framework:
|
||||
messenger:
|
||||
failure_transport: failed
|
||||
|
||||
transports:
|
||||
# https://symfony.com/doc/current/messenger.html#transport-configuration
|
||||
async:
|
||||
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
|
||||
options:
|
||||
use_notify: true
|
||||
check_delayed_interval: 60000
|
||||
retry_strategy:
|
||||
max_retries: 3
|
||||
multiplier: 2
|
||||
failed: 'doctrine://default?queue_name=failed'
|
||||
# sync: 'sync://'
|
||||
|
||||
default_bus: messenger.bus.default
|
||||
|
||||
buses:
|
||||
messenger.bus.default: []
|
||||
|
||||
routing:
|
||||
Symfony\Component\Mailer\Messenger\SendEmailMessage: async
|
||||
Symfony\Component\Notifier\Message\ChatMessage: async
|
||||
Symfony\Component\Notifier\Message\SmsMessage: async
|
||||
|
||||
# Route your messages to the transports
|
||||
# 'App\Message\YourMessage': async
|
||||
@@ -0,0 +1,62 @@
|
||||
monolog:
|
||||
channels:
|
||||
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
|
||||
|
||||
when@dev:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event"]
|
||||
# uncomment to get logging in your browser
|
||||
# you may have to allow bigger header sizes in your Web server configuration
|
||||
#firephp:
|
||||
# type: firephp
|
||||
# level: info
|
||||
#chromephp:
|
||||
# type: chromephp
|
||||
# level: info
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!console"]
|
||||
|
||||
when@test:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
channels: ["!event"]
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
|
||||
when@prod:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||
nested:
|
||||
type: stream
|
||||
path: php://stderr
|
||||
level: debug
|
||||
formatter: monolog.formatter.json
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine"]
|
||||
deprecation:
|
||||
type: stream
|
||||
channels: [deprecation]
|
||||
path: php://stderr
|
||||
formatter: monolog.formatter.json
|
||||
@@ -0,0 +1,12 @@
|
||||
framework:
|
||||
notifier:
|
||||
chatter_transports:
|
||||
texter_transports:
|
||||
channel_policy:
|
||||
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
|
||||
urgent: ['email']
|
||||
high: ['email']
|
||||
medium: ['email']
|
||||
low: ['email']
|
||||
admin_recipients:
|
||||
- { email: admin@example.com }
|
||||
@@ -0,0 +1,10 @@
|
||||
framework:
|
||||
router:
|
||||
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||
#default_uri: http://localhost
|
||||
|
||||
when@prod:
|
||||
framework:
|
||||
router:
|
||||
strict_requirements: null
|
||||
@@ -0,0 +1,32 @@
|
||||
security:
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||
providers:
|
||||
app_user_provider:
|
||||
memory:
|
||||
users:
|
||||
me@joeac.net:
|
||||
password: '%env(resolve:JOEAC_PASSWORD)%'
|
||||
roles: ['ROLE_EDITOR']
|
||||
firewalls:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
main:
|
||||
lazy: true
|
||||
provider: app_user_provider
|
||||
form_login:
|
||||
login_path: login
|
||||
check_path: login
|
||||
enable_csrf: true
|
||||
logout:
|
||||
path: logout
|
||||
|
||||
when@test:
|
||||
security:
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||
algorithm: auto
|
||||
cost: 4 # Lowest possible value for bcrypt
|
||||
time_cost: 3 # Lowest possible value for argon
|
||||
memory_cost: 10 # Lowest possible value for argon
|
||||
@@ -0,0 +1,7 @@
|
||||
framework:
|
||||
default_locale: en
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
fallbacks:
|
||||
- en
|
||||
providers:
|
||||
@@ -0,0 +1,6 @@
|
||||
twig:
|
||||
file_name_pattern: '*.twig'
|
||||
|
||||
when@test:
|
||||
twig:
|
||||
strict_variables: true
|
||||
@@ -0,0 +1,11 @@
|
||||
framework:
|
||||
validation:
|
||||
# Enables validator auto-mapping support.
|
||||
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
|
||||
#auto_mapping:
|
||||
# App\Entity\: []
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
validation:
|
||||
not_compromised_password: false
|
||||
@@ -0,0 +1,11 @@
|
||||
when@dev:
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
|
||||
framework:
|
||||
profiler:
|
||||
collect_serializer_data: true
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
profiler: { collect: false }
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
|
||||
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
controllers:
|
||||
resource:
|
||||
path: ../src/Controller/
|
||||
namespace: App\Controller
|
||||
type: attribute
|
||||
@@ -0,0 +1,4 @@
|
||||
when@dev:
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /_error
|
||||
@@ -0,0 +1,3 @@
|
||||
_security_logout:
|
||||
resource: security.route_loader.logout
|
||||
type: service
|
||||
@@ -0,0 +1,8 @@
|
||||
when@dev:
|
||||
web_profiler_wdt:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
|
||||
prefix: /_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
|
||||
prefix: /_profiler
|
||||
@@ -0,0 +1,25 @@
|
||||
# This file is the entry point to configure your own services.
|
||||
# Files in the packages/ subdirectory configure your dependencies.
|
||||
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
_defaults:
|
||||
autowire: true # Automatically injects dependencies in your services.
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/'
|
||||
exclude:
|
||||
- '../src/DependencyInjection/'
|
||||
- '../src/Entity/'
|
||||
- '../src/Interface/'
|
||||
- '../src/Kernel.php'
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Returns the importmap for this application.
|
||||
*
|
||||
* - "path" is a path inside the asset mapper system. Use the
|
||||
* "debug:asset-map" command to see the full list of paths.
|
||||
*
|
||||
* - "entrypoint" (JavaScript only) set to true for any module that will
|
||||
* be used as an "entrypoint" (and passed to the importmap() Twig function).
|
||||
*
|
||||
* The "importmap:require" command can be used to add new entries to this file.
|
||||
*/
|
||||
return [
|
||||
'app' => [
|
||||
'path' => './assets/app.js',
|
||||
'entrypoint' => true,
|
||||
],
|
||||
'@hotwired/stimulus' => [
|
||||
'version' => '3.2.2',
|
||||
],
|
||||
'@symfony/stimulus-bundle' => [
|
||||
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
|
||||
],
|
||||
'@hotwired/turbo' => [
|
||||
'version' => '7.3.0',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250517200832 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE note (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, content VARCHAR(255) NOT NULL, published_date DATE NOT NULL)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE messenger_messages (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, body CLOB NOT NULL, headers CLOB NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
|
||||
, available_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
|
||||
, delivered_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
|
||||
)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at)
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE note
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE messenger_messages
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250517212750 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE note ADD COLUMN slug VARCHAR(255) NOT NULL
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TEMPORARY TABLE __temp__note AS SELECT id, content, published_date FROM note
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE note
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE note (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, content VARCHAR(255) NOT NULL, published_date DATE NOT NULL)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
INSERT INTO note (id, content, published_date) SELECT id, content, published_date FROM __temp__note
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE __temp__note
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20250519215136 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void {}
|
||||
public function down(Schema $schema): void {}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20250521000000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string {
|
||||
return 'Migrating my links from my old website to notes in my new website';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void {
|
||||
$this->addSql(<<<'SQL'
|
||||
INSERT INTO note(content, published_date, slug) VALUES
|
||||
('[Is Stack Overflow Obsolete? An Empirical Study of the Characteristics of ChatGPT Answers to Stack Overflow Questions](https://dl.acm.org/doi/pdf/10.1145/3613904.3642596). ChatGPT 3.5 gives bad answers half the time, and programmers miss the mistakes almost half the time. Be careful out there, folks.', '2024-06-04', '2024-06-04'),
|
||||
('[Primary and Secondary Values](https://www.colinmcginn.net/primary-and-secondary-values). Colin McGinn makes the case for primary and secondary moral values, just as there are primary and secondary qualities, apparently thereby managing to assert both moral realism and anti-realism at the same time without contradiction.', '2024-06-05', '2024-06-05'),
|
||||
('[Pluralistic: They brick you because they can](https://pluralistic.net/2024/05/24/record-scratch/#autoenshittification). Cory Doctorow writes incessantly about the harms of monopolised markets. This essay is particularly good, because he collects many of monopolists’ greatest hits from recent years. Do keep reading to the end. It just gets better.', '2024-06-05', '2024-06-05-1'),
|
||||
('[Parenting](https://johan.hal.se/wrote/2024/06/05/parenting). ‘Once you find yourself in the position of being someone’s father you’ll quickly realize that you’re not actually raising anyone here, you just happen to be the veteran in the trenches alongside them, showing them the ropes and hoping they’ll survive and turn out okay.’', '2024-06-05', '2024-06-05-2'),
|
||||
('[Boys](https://www.bbc.co.uk/programmes/m001yshl). Catherine Carr did a fantastic job of unveiling how teenage boys are experiencing masculinity in Britain today. Plenty here to surprise, shock and inspire.', '2024-06-05', '2024-06-05-3'),
|
||||
('[BCS manifesto](https://www.bcs.org/articles-opinion-and-research/the-computing-revolution-how-the-next-government-can-transform-society-with-ethics-education-and-equity-in-technology). Good for what it is. Good policies. Succinct. Should be the beginning (<em>not</em> the end) of some interesting conversations.', '2024-06-05', '2024-06-05-4'),
|
||||
('[154 McDonald Road: Gone but not Forgotten](https://broughtonspurtle.org.uk/news/gone-not-forgotten). A superb tribute to the building and analysis of the failures of the planning system. This was published in my free local newsletter, and is worthy of any broadsheet newspaper.', '2024-06-07', '2024-06-07'),
|
||||
('[Beware the cloud of hype](https://thehistoryoftheweb.com/beware-the-cloud-of-hype). Jay Hoffman spots some striking parallels between the current AI hype and the dot-com bubble.', '2024-06-07', '2024-06-07-1'),
|
||||
('[Saving the News from Big Tech](https://eff.org/saving-the-news). Cory Doctorow, writing for the Electronic Frontier Foundation, argues that to save news media, we need to dismantle ad-tech monopolies, ban surveillance advertising, open up app stores and have an end-to-end web.', '2024-06-07', '2024-06-07-2'),
|
||||
('[Instagram is training AI on your data. It’s nearly impossible to opt out](https://www.fastcompany.com/91132854/instagram-training-ai-on-your-data-its-nearly-impossible-to-opt-out). Yuck, yuck, yuck. Makes me glad I’m not on Instagram. For people already stuck there, though, this just sucks. Highly recommend either opting out of AI training or quitting Insta, if only to give the twits the middle finger they deserve.', '2024-06-14', '2024-06-14'),
|
||||
('[State-based UI is an anti-pattern](https://gomakethings.com/state-based-ui-is-an-anti-pattern). Chris Ferdinandi has a hot take here. I would be keen to test this idea out one day: push the limits of how much complex state you can manage within the light DOM.', '2024-06-14', '2024-06-14-1'),
|
||||
('[Big Data is Dead](https://motherduck.com/blog/big-data-is-dead). Did you know that most organisations store less than 100GB, and almost all analytics is run on the last 24h of data? I didn’t. Though take it all with a pinch of salt: the guy’s writing on his company blog which sells traditional data warehouses.', '2024-06-17', '2024-06-17'),
|
||||
('[Reverse Engineering TicketMaster''s Rotating Barcodes (SafeTix)](https://conduition.io/coding/ticketmaster). Who doesn''t like a classic David-and-Goliath hacker story? Also, if you''re American, please <a href="https://www.breakupticketmaster.com">break up TicketMaster</a>. If you''re in the UK, it''s not quite as bad, but <a href="https://assets.publishing.service.gov.uk/media/5519473540f0b61401000087/final_report.pdf">it''s still really bad</a>. Use alternatives where you can.', '2024-07-16', '2024-07-16'),
|
||||
('[Don’t use booleans](https://www.luu.io/posts/dont-use-booleans). A nice idea. But I think this advice only applies well when you''ve got many inter-dependent flags. If you have independent flags, re-writing those as enums will just end up with you re-implementing the boolean type for every parameter, and not getting much profit, I reckon.', '2024-07-16', '2024-07-16-1'),
|
||||
('[Story points are wasting time](https://blog.scottlogic.com/2024/07/05/story-points-are-wasting-time.html). Pretty convincing to me. The biggest potential weakness in his argument is his claim that none of the most common reasons why devs disagree on story points exposes anything which ought to be resolved in an estimation meeting. If you can provide other common reasons besides the ones Dave considered, you could rebut his argument. I don''t feel experienced enough to judge this myself.', '2024-07-17', '2024-07-17'),
|
||||
('[Goldman Sachs Top of the Mind, Issue 129](https://www.goldmansachs.com/intelligence/pages/gs-research/gen-ai-too-much-spend-too-little-benefit/report.pdf). Read the interviews. Economists give interesting, and diverse, opinions on the economic potential of LLMs.', '2024-07-18', '2024-07-18'),
|
||||
('[The Human Cost Of Our AI-Driven Future](https://www.noemamag.com/the-human-cost-of-our-ai-driven-future). In case you''d forgotten: content moderation is still carried out by appalling worker exploitation. This is not news, but nonetheless an excellent and suitably chilling essay on the topic. Be aware that the essay describes some deeply traumatic content.', '2024-10-11', '2024-10-11'),
|
||||
('[LLMs don''t ''hallucinate''](https://blog.scottlogic.com/2024/09/10/llms-dont-hallucinate.html). I posted on the Scott Logic blog a while ago about how the word ''hallucination'' doesn''t accurately capture how LLMs work.', '2024-10-11', '2024-10-11-1'),
|
||||
('[Please just stop saying ''just''](https://sgringwe.com/2019/10/10/Please-just-stop-saying-just.html). A pretty good case for avoiding the word ''just'' in software engineering. I admit I''ve been guilty, too.', '2024-11-11', '2024-11-11'),
|
||||
('[Sexual symmetry and asymmetry](https://alexanderpruss.blogspot.com/2024/11/sexual-symmetry-and-asymmetry.html). Alexander Pruss has a bizarre, but at first blush convincing, argument that complementarians about gender don’t have to appeal to morally significant intrinsic differences between men and women.', '2024-12-17', '2024-12-17'),
|
||||
('["Rules" that terminal programs follow](https://jvns.ca/blog/2024/11/26/terminal-rules). Handy for the next time you develop a CLI or TUI. Also handy as a user: now I know about <a href="https://readline.kablamo.org/emacs.html">readline key bindings</a>, which are everywhere apparently.', '2024-12-20', '2024-12-20'),
|
||||
('[Bank of England''s ''Millenium of Macroeconomic Data''](https://www.bankofengland.co.uk/statistics/research-datasets). There was no long-term price inflation from 1200 (when these data begin) until 1550. WHAT?!', '2025-01-23', '2025-01-23'),
|
||||
('[Free social media from billionaire control](https://freeourfeeds.com). I just donated $40. These guys are promising to do whatever it takes to make sure the AT Protocol is genuinely owned by everyone.', '2025-01-28', '2025-01-28')
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void {
|
||||
$this->addSql(<<<'SQL'
|
||||
DELETE FROM note WHERE
|
||||
slug = '2024-06-04' OR
|
||||
slug = '2024-06-05' OR
|
||||
slug = '2024-06-05-1' OR
|
||||
slug = '2024-06-05-2' OR
|
||||
slug = '2024-06-05-3' OR
|
||||
slug = '2024-06-05-4' OR
|
||||
slug = '2024-06-07' OR
|
||||
slug = '2024-06-07-1' OR
|
||||
slug = '2024-06-07-2' OR
|
||||
slug = '2024-06-14' OR
|
||||
slug = '2024-06-14-1' OR
|
||||
slug = '2024-06-17' OR
|
||||
slug = '2024-07-16' OR
|
||||
slug = '2024-07-16-1' OR
|
||||
slug = '2024-07-17' OR
|
||||
slug = '2024-07-18' OR
|
||||
slug = '2024-10-11' OR
|
||||
slug = '2024-10-11-1' OR
|
||||
slug = '2024-11-11' OR
|
||||
slug = '2024-12-17' OR
|
||||
slug = '2024-12-20' OR
|
||||
slug = '2025-01-23' OR
|
||||
slug = '2025-01-28'
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250522212213 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE blog_post (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, slug VARCHAR(255) NOT NULL, published_date DATE NOT NULL, updated_date DATE DEFAULT NULL, content CLOB NOT NULL, title VARCHAR(255) NOT NULL, description VARCHAR(1024) NOT NULL)
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE blog_post
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use DateTime;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
final class Version20250522212300 extends AbstractMigration
|
||||
{
|
||||
private const BLOG_POST_SLUGS = [
|
||||
'doctor_who_gayness_church',
|
||||
'does_resurrection_ground_works',
|
||||
'easter',
|
||||
'euhwc_toast_to_the_lasses_2024',
|
||||
'god_is_not_great',
|
||||
'llms_do_not_understand_anything',
|
||||
'my_feed_and_reading_list',
|
||||
'no_more_youtube',
|
||||
'open_questions_about_sex',
|
||||
'paradox',
|
||||
'sapiens_on_religion',
|
||||
'science_and_philosophy',
|
||||
'surprised_by_hope',
|
||||
'tracking_pixels',
|
||||
'who_consecrates_the_temple',
|
||||
'word_hallucination_with_reference_to_llms',
|
||||
];
|
||||
|
||||
public function getDescription(): string {
|
||||
return 'Migrating my blog posts from my old website to notes in my new website';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void {
|
||||
foreach (self::BLOG_POST_SLUGS as $slug) {
|
||||
$blogPostPath = 'scripts/blog-migrated/' . $slug . '.yaml';
|
||||
$blogPost = Yaml::parseFile($blogPostPath);
|
||||
|
||||
$publishedDateTime = DateTime::createFromFormat('U', strval($blogPost['pubDate']));
|
||||
$publishedDate = $publishedDateTime->format('Y-m-d');
|
||||
$updatedDateTime = array_key_exists(array: $blogPost, key: 'updatedDate')
|
||||
? DateTime::createFromFormat('U', strval($blogPost['updatedDate']))
|
||||
: null;
|
||||
$updatedDate = $updatedDateTime == null
|
||||
? 'NULL'
|
||||
: "'" . $updatedDateTime->format('Y-m-d') . "'";
|
||||
$title = str_replace("'", "''", $blogPost['title']);
|
||||
$description = str_replace("'", "''", $blogPost['description']);
|
||||
$content = str_replace("'", "''", $blogPost['content']);
|
||||
|
||||
$this->addSql(<<<SQL
|
||||
INSERT INTO blog_post(slug, published_date, updated_date, title, description, content)
|
||||
VALUES('$slug', '$publishedDate', $updatedDate, '$title', '$description', '$content')
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void {
|
||||
foreach (self::BLOG_POST_SLUGS as $slug) {
|
||||
$this->addSql(<<<SQL
|
||||
DELETE FROM blog_post WHERE slug = '$slug'
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
convertDeprecationsToExceptions="false"
|
||||
>
|
||||
<php>
|
||||
<ini name="display_errors" value="1" />
|
||||
<ini name="error_reporting" value="-1" />
|
||||
<server name="APP_ENV" value="test" force="true" />
|
||||
<server name="SHELL_VERBOSITY" value="-1" />
|
||||
<server name="SYMFONY_PHPUNIT_REMOVE" value="" />
|
||||
<server name="SYMFONY_PHPUNIT_VERSION" value="9.5" />
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Project Test Suite">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
|
||||
<listeners>
|
||||
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
|
||||
</listeners>
|
||||
|
||||
<extensions>
|
||||
</extensions>
|
||||
</phpunit>
|
||||
@@ -0,0 +1,316 @@
|
||||
/** Variables */
|
||||
|
||||
:root {
|
||||
--colour-primary-10: #060300;
|
||||
--colour-primary-15: #150800;
|
||||
--colour-primary-20: #1f1400;
|
||||
--colour-primary-30: #3c2b00;
|
||||
--colour-primary-40: #5c4300;
|
||||
--colour-primary-50: #7f5d00;
|
||||
--colour-primary-60: #a37800;
|
||||
--colour-primary-70: #c89500;
|
||||
--colour-primary-80: #efb300;
|
||||
--colour-primary-90: #ffd98c;
|
||||
--colour-primary-95: #ffecc8;
|
||||
|
||||
--colour-hyperlink-10: #000409;
|
||||
--colour-hyperlink-20: #001829;
|
||||
--colour-hyperlink-30: #00314d;
|
||||
--colour-hyperlink-40: #004d75;
|
||||
--colour-hyperlink-50: #006a9f;
|
||||
--colour-hyperlink-60: #1388c9;
|
||||
--colour-hyperlink-70: #41a8ea;
|
||||
--colour-hyperlink-80: #78c7ff;
|
||||
--colour-hyperlink-90: #bfe3ff;
|
||||
--colour-hyperlink-95: #e0f1ff;
|
||||
|
||||
--colour-primary-fg: var(--colour-primary-90);
|
||||
--colour-primary-fg-accent: var(--colour-primary-80);
|
||||
--colour-primary-bg: var(--colour-primary-10);
|
||||
--colour-primary-bg-accent: var(--colour-primary-20);
|
||||
--colour-code-fg: var(--colour-primary-90);
|
||||
--colour-code-bg: var(--colour-primary-15);
|
||||
--colour-hyperlink: var(--colour-hyperlink-80);
|
||||
|
||||
--font-size-sm: 1rem;
|
||||
--font-size-base: 1.125rem;
|
||||
--font-size-md: 1.5rem;
|
||||
--font-size-lg: 2rem;
|
||||
--font-size-xl: 3rem;
|
||||
|
||||
--spacing-block-xs: 0.5rem;
|
||||
--spacing-block-sm: 1.75rem;
|
||||
--spacing-block-md: 2.5rem;
|
||||
--spacing-block-lg: 3.5rem;
|
||||
--spacing-block-xl: 5rem;
|
||||
--spacing-inline-xs: 0.25rem;
|
||||
--spacing-inline-sm: 0.5rem;
|
||||
--spacing-inline-md: 1.5rem;
|
||||
--spacing-inline-lg: 3rem;
|
||||
--spacing-inline-xl: 6rem;
|
||||
}
|
||||
|
||||
/** Light theme */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--colour-primary-fg: var(--colour-primary-20);
|
||||
--colour-primary-fg-accent: var(--colour-primary-40);
|
||||
--colour-primary-bg: var(--colour-primary-95);
|
||||
--colour-primary-bg-accent: var(--colour-primary-90);
|
||||
--colour-hyperlink: var(--colour-hyperlink-40);
|
||||
}
|
||||
}
|
||||
|
||||
/** Base typography */
|
||||
|
||||
body {
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--colour-primary-fg);
|
||||
font-weight: light;
|
||||
background-color: var(--colour-primary-bg);
|
||||
line-height: 1.5;
|
||||
|
||||
/* Geometric Humanist stack from https://modernfontstacks.com */
|
||||
font-family: Avenir, Montserrat, Corbel, 'URW Gothic', source-sans-pro, sans-serif;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
:is(p, h1, h2, h3, h4, h5, h6, hr, img, figure, ul, ol) {
|
||||
margin-block-start: var(--spacing-block-sm);
|
||||
}
|
||||
|
||||
/** Base layout */
|
||||
|
||||
body {
|
||||
--body-margin-inline-start: var(--spacing-inline-lg);
|
||||
--body-margin-inline-end: var(--body-margin-inline-start);
|
||||
--body-margin-block-end: var(--spacing-block-xl);
|
||||
margin-inline: var(--body-margin-inline-start) var(--body-margin-inline-end);
|
||||
margin-block-end: var(--body-margin-block-end);
|
||||
}
|
||||
|
||||
:is(h1, h2, h3, h4, h5, h6) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-inline: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 48rem) {
|
||||
body {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
[media-start]
|
||||
var(--grid-margin-inline)
|
||||
[content-start]
|
||||
minmax(var(--grid-max-content-width), auto)
|
||||
[content-end];
|
||||
column-gap: var(--spacing-block-sm);
|
||||
max-width: var(--grid-total-width);
|
||||
|
||||
--body-margin-inline-end: 6rem;
|
||||
--grid-margin-inline: 6rem;
|
||||
--grid-total-width: 48rem;
|
||||
--grid-max-content-width: calc(
|
||||
var(--grid-total-width)
|
||||
- var(--body-margin-inline-start)
|
||||
- var(--grid-margin-inline)
|
||||
- var(--spacing-block-sm)
|
||||
- var(--grid-margin-inline)
|
||||
);
|
||||
}
|
||||
|
||||
:is(main, article, nav) {
|
||||
display: grid;
|
||||
grid-column: media-start / content-end;
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
|
||||
:is(section, header, aside) {
|
||||
grid-column: content-start / content-end;
|
||||
}
|
||||
|
||||
:is(h1, h2, h3, h4, h5, h6) {
|
||||
text-align: start;
|
||||
}
|
||||
}
|
||||
|
||||
/** Headings */
|
||||
|
||||
h1 {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 900;
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 900;
|
||||
margin-block-start: var(--spacing-block-xl);
|
||||
}
|
||||
|
||||
h3, h4, h5, h6 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
margin-block-start: var(--spacing-block-lg);
|
||||
}
|
||||
|
||||
/** Hyperlinks */
|
||||
|
||||
:is(:link, :visited) {
|
||||
color: var(--colour-hyperlink);
|
||||
text-decoration: underline;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
:hover {
|
||||
text-decoration: wavy;
|
||||
}
|
||||
|
||||
/** Definition lists */
|
||||
dl {
|
||||
margin-block-start: var(--spacing-block-md);
|
||||
}
|
||||
|
||||
dl dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
dl dd + dt {
|
||||
margin-block-start: var(--spacing-block-md);
|
||||
}
|
||||
|
||||
/** figcaptions */
|
||||
|
||||
figcaption {
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/** Lists */
|
||||
|
||||
:is(ol, ul) {
|
||||
margin-inline-start: var(--spacing-inline-lg);
|
||||
}
|
||||
|
||||
/** Navigation bar */
|
||||
|
||||
.navbar {
|
||||
margin-block: var(--spacing-block-sm);
|
||||
}
|
||||
|
||||
.navbar ul {
|
||||
grid-column: media-start / content-end;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-inline-md);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 36rem) {
|
||||
.navbar {
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
|
||||
.navbar ul {
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
|
||||
/** Emphasis */
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/** Blog feed */
|
||||
|
||||
.h-feed ul {
|
||||
list-style: none;
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
/** Block quotes */
|
||||
blockquote {
|
||||
padding-inline-start: var(--spacing-inline-lg);
|
||||
border-inline-start: 2px solid var(--colour-primary-fg);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
blockquote footer {
|
||||
font-style: initial;
|
||||
}
|
||||
|
||||
blockquote :is(b, strong) {
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
blockquote :is(i, em) {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/** Small caps */
|
||||
.small-caps {
|
||||
font-variant: small-caps;
|
||||
}
|
||||
|
||||
/** Pre-formatted blocks */
|
||||
pre {
|
||||
border: 2px solid var(--colour-primary-fg);
|
||||
border-radius: 2px;
|
||||
background-color: var(--colour-code-bg) !important;
|
||||
margin-block-start: var(--spacing-block-sm);
|
||||
padding-inline: var(--spacing-inline-sm);
|
||||
padding-block: var(--spacing-block-xs);
|
||||
}
|
||||
|
||||
/** Code blocks */
|
||||
code {
|
||||
border: 2px solid var(--colour-primary-fg);
|
||||
border-radius: 2px;
|
||||
padding-inline: var(--spacing-inline-xs);
|
||||
color: var(--colour-code-fg);
|
||||
background-color: var(--colour-code-bg);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
pre code {
|
||||
border: none;
|
||||
border-radius: none;
|
||||
padding: none;
|
||||
}
|
||||
|
||||
/** Alerts */
|
||||
.alert {
|
||||
border: 2px solid var(--colour-primary-fg);
|
||||
border-radius: 2px;
|
||||
padding-inline: var(--spacing-inline-xs);
|
||||
background-color: var(--color-primary-fg-accent);
|
||||
}
|
||||
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
|
||||
white-space: nowrap;
|
||||
clip-path: inset(100%);
|
||||
clip: rect(0 0 0 0);
|
||||
overflow: hidden;
|
||||
|
||||
outline: none;
|
||||
outline-offset: 0;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
div:has(img) {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
div img {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
border-radius: 1rem;
|
||||
filter: contrast(1.25);
|
||||
}
|
||||
|
||||
div:has(img)::after {
|
||||
/* Colour overlay */
|
||||
background-color: var(--colour-primary-80);
|
||||
opacity: 0.3;
|
||||
|
||||
/* Same size and shape as the img */
|
||||
border-radius: 1rem;
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
|
||||
/* Positioned on top of the img */
|
||||
display: block;
|
||||
position: relative;
|
||||
top: -6rem;
|
||||
|
||||
/* A content value is needed to get the ::after to render */
|
||||
content: '';
|
||||
}
|
||||
|
||||
header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
text-align: center;
|
||||
border-block-end: 1px solid var(--colour-primary-fg);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
grid-column: 1 / 3;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.technical-skills h3 {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.technical-skills ul {
|
||||
color: var(--colour-primary-fg-accent);
|
||||
margin-inline-start: var(--spacing-inline-md);
|
||||
}
|
||||
|
||||
@media (min-width: 46rem) {
|
||||
.technical-skills section {
|
||||
display: flex;
|
||||
gap: var(--spacing-inline-sm);
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.technical-skills section h3::after {
|
||||
content: '/';
|
||||
margin-inline: var(--spacing-inline-sm);
|
||||
}
|
||||
|
||||
.technical-skills section ul {
|
||||
display: flex;
|
||||
gap: var(--spacing-inline-sm);
|
||||
list-style: none;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.technical-skills section ul li + li::before {
|
||||
content: '•';
|
||||
margin-inline-end: var(--spacing-inline-sm);
|
||||
}
|
||||
}
|
||||
|
||||
:is(.experience, .passions) :is(ol, ul) {
|
||||
list-style: none;
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
:is(.experience, .passions) :is(ol, ul) li {
|
||||
margin-block-start: var(--spacing-block-sm);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
p:has(.dt-published, .dt-updated) {
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: italic;
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
.p-summary {
|
||||
font-style: italic;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-block-start: var(--spacing-block-md);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
h2 {
|
||||
margin-block-start: var(--spacing-block-md);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-block-start: var(--spacing-block-sm);
|
||||
}
|
||||
|
||||
.h-entry {
|
||||
background-color: var(--colour-primary-bg-accent);
|
||||
border: 0.125rem solid var(--colour-primary-fg);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
.h-card div:has(img) {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.h-card img {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
border-radius: 1rem;
|
||||
filter: contrast(1.25);
|
||||
}
|
||||
|
||||
.h-card div:has(img)::after {
|
||||
/* Colour overlay */
|
||||
background-color: var(--colour-primary-80);
|
||||
opacity: 0.3;
|
||||
|
||||
/* Same size and shape as the img */
|
||||
border-radius: 1rem;
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
|
||||
/* Positioned on top of the img */
|
||||
display: block;
|
||||
position: relative;
|
||||
top: -6rem;
|
||||
|
||||
/* A content value is needed to get the ::after to render */
|
||||
content: '';
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 36rem) {
|
||||
.h-card {
|
||||
grid-column: media-start / content-end;
|
||||
display: grid;
|
||||
grid-template-columns: subgrid; /** Subgrid of main column layout */
|
||||
grid-template-rows: min-content 1fr;
|
||||
grid-template-areas:
|
||||
"empty heading"
|
||||
"photo text";
|
||||
}
|
||||
|
||||
.h-card div:has(img) {
|
||||
grid-area: photo;
|
||||
margin-block-start: var(--spacing-block-sm);
|
||||
}
|
||||
|
||||
.h-card header {
|
||||
grid-area: heading;
|
||||
}
|
||||
|
||||
.h-card__text {
|
||||
grid-area: text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/* Based on Andy Bell’s More Modern CSS Reset: https://piccalil.li/blog/a-more-modern-css-reset/ */
|
||||
|
||||
/* Box sizing rules */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Prevent font size inflation */
|
||||
html {
|
||||
-moz-text-size-adjust: none;
|
||||
-webkit-text-size-adjust: none;
|
||||
text-size-adjust: none;
|
||||
}
|
||||
|
||||
/* Remove default margin in favour of better control in authored CSS */
|
||||
body, h1, h2, h3, h4, p,
|
||||
figure, blockquote, dl, dd {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
|
||||
ul[role='list'],
|
||||
ol[role='list'] {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* Set core body defaults */
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Set shorter line heights on headings and interactive elements */
|
||||
h1, h2, h3, h4,
|
||||
button, input, label {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* Balance text wrapping on headings */
|
||||
h1, h2,
|
||||
h3, h4 {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
/* Make images easier to work with */
|
||||
img,
|
||||
picture {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Inherit fonts for inputs and buttons */
|
||||
input, button,
|
||||
textarea, select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
/* Make sure textareas without a rows attribute are not tiny */
|
||||
textarea:not([rows]) {
|
||||
min-height: 10em;
|
||||
}
|
||||
|
||||
/* Anything that has been anchored to should have extra scroll margin */
|
||||
:target {
|
||||
scroll-margin-block: 5ex;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 949 KiB |
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
return function (array $context) {
|
||||
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
};
|
||||
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\BlogPost;
|
||||
use App\Entity\Note;
|
||||
use App\Form\Type\NoteType;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bridge\Twig\Attribute\Template;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
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 {
|
||||
/**
|
||||
* @return array<string,string>
|
||||
*/
|
||||
#[Route('/', name: 'index')]
|
||||
#[Template('/index.html.twig')]
|
||||
public function index(): array {
|
||||
return [
|
||||
'title' => 'Joe Carstairs',
|
||||
'description' => 'Joe Carstairs\' personal website',
|
||||
];
|
||||
}
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
#[Route('/notes', name: 'notes')]
|
||||
#[Template('/notes.html.twig')]
|
||||
public function notes(
|
||||
EntityManagerInterface $entityManager,
|
||||
): array {
|
||||
$notes = $entityManager->getRepository(Note::class)->findAllOrderedBySlugDesc();
|
||||
$years = $this->getYears($notes);
|
||||
$notesByYear = $this->groupByYear($notes);
|
||||
$months = array_map(array: $years, callback: fn($year) => $this->getMonths($notesByYear[$year]));
|
||||
$monthsByYear = array_combine(keys: $years, values: $months);
|
||||
$groupByMonth = fn($notes) => $this->groupByMonth($notes);
|
||||
$notesByYearAndMonth = array_map(
|
||||
array: $notesByYear,
|
||||
callback: $groupByMonth,
|
||||
);
|
||||
|
||||
return [
|
||||
'title' => 'Joe Carstairs\' notes',
|
||||
'description' => 'Joe Carstairs\' notes',
|
||||
'isFeed' => True,
|
||||
'notes' => $notesByYearAndMonth,
|
||||
'years' => $years,
|
||||
'months' => $monthsByYear,
|
||||
'monthNames' => MONTH_NAMES,
|
||||
];
|
||||
}
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
#[Route('/blog', name: 'blog_posts')]
|
||||
#[Template('/blog_posts.html.twig')]
|
||||
public function blogPosts(
|
||||
EntityManagerInterface $entityManager,
|
||||
): array {
|
||||
$posts = $entityManager->getRepository(BlogPost::class)->findAllOrderedBySlugDesc();
|
||||
$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,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @@template T of \FeedEntry
|
||||
* @param $entries T[]
|
||||
* @return T[][]
|
||||
*/
|
||||
function groupByYear(array $entries): array {
|
||||
$years = $this->getYears($entries);
|
||||
$filterByYear = fn(string $year) =>
|
||||
fn($entry) => ($entry->getPublishedDate()->format('Y') == $year);
|
||||
$entriesForYear = fn(string $year) =>
|
||||
array_filter(array: $entries, callback: $filterByYear($year));
|
||||
$entriesByYear = array_map(
|
||||
array: $years,
|
||||
callback: $entriesForYear,
|
||||
);
|
||||
return array_combine(keys: $years, values: $entriesByYear);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of \FeedEntry
|
||||
* @param $entries T[]
|
||||
* @return string[]
|
||||
*/
|
||||
function getYears(array $entries): array {
|
||||
$getYear = fn($note): string => $note->getPublishedDate()->format('Y');
|
||||
$years = array_map(array: $entries, callback: $getYear);
|
||||
$years = array_unique($years);
|
||||
arsort($years);
|
||||
return $years;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of \FeedEntry
|
||||
* @param $entries T[]
|
||||
* @return T[]
|
||||
*/
|
||||
function groupByMonth(array $entries): array {
|
||||
$months = $this->getMonths($entries);
|
||||
$filterByMonth = fn(string $month) =>
|
||||
fn($entry) => ($entry->getPublishedDate()->format('m') == $month);
|
||||
$entriesByMonth = array_map(
|
||||
array: $months,
|
||||
callback: fn(string $month) =>
|
||||
array_filter(array: $entries, callback: $filterByMonth($month)),
|
||||
);
|
||||
return array_combine(keys: $months, values: $entriesByMonth);
|
||||
}
|
||||
|
||||
/**
|
||||
* @@template T of \FeedEntry
|
||||
* @param $entries T[]
|
||||
* @return string[]
|
||||
*/
|
||||
function getMonths(array $entries): array {
|
||||
$getMonth = fn($entry) => $entry->getPublishedDate()->format('m');
|
||||
$months = array_map(array: $entries, callback: $getMonth);
|
||||
$months = array_unique($months);
|
||||
arsort($months);
|
||||
return $months;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of \FeedEntry
|
||||
* @param $entries T[]
|
||||
* @return T[]
|
||||
*/
|
||||
function sortBySlug(array $entries): array {
|
||||
$getSlug = fn($entry): string => $entry->getSlug();
|
||||
$slugs = array_map(array: $entries, callback: $getSlug);
|
||||
$entriesBySlug = array_combine(keys: $slugs, values: $entries);
|
||||
krsort($entriesBySlug);
|
||||
return array_values($entriesBySlug);
|
||||
}
|
||||
|
||||
#[IsGranted('ROLE_EDITOR')]
|
||||
#[Route('/notes/write')]
|
||||
#[Template('/write_note.html.twig')]
|
||||
public function writeNote(
|
||||
EntityManagerInterface $entityManager,
|
||||
Request $request,
|
||||
): Response {
|
||||
$note = new Note();
|
||||
$form = $this->createForm(NoteType::class, $note);
|
||||
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$note = $form->getData();
|
||||
|
||||
$now = new DateTime('now');
|
||||
$note->setPublishedDate($now);
|
||||
|
||||
$num_existing_notes_today = $entityManager->getRepository(Note::class)->countWherePublishedOnDate($now);
|
||||
$note->getPublishedDate()->setTimezone(new DateTimeZone('Europe/London'));
|
||||
$slug = $note->getPublishedDate()->format('Y-m-d');
|
||||
if ($num_existing_notes_today > 0) {
|
||||
$slug = $slug . '-' . $num_existing_notes_today;
|
||||
}
|
||||
$note->setSlug($slug);
|
||||
|
||||
$entityManager->persist($note);
|
||||
$entityManager->flush();
|
||||
|
||||
return $this->redirectToRoute('note', ['slug' => $slug]);
|
||||
}
|
||||
|
||||
return $this->render('/write_note.html.twig', [
|
||||
'title' => 'Write for Joe Carstairs',
|
||||
'description' => 'The authoring page for Joe Carstairs\' personal website',
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
#[Route('/notes/{slug}', name: 'note')]
|
||||
#[Template('/note.html.twig')]
|
||||
public function note(
|
||||
EntityManagerInterface $entityManager,
|
||||
string $slug,
|
||||
): array {
|
||||
$repository = $entityManager->getRepository(Note::class);
|
||||
$note = $repository->findOneBy(['slug' => $slug]);
|
||||
|
||||
return [
|
||||
'title' => 'Joe Carstairs\' notes',
|
||||
'description' => 'Joe Carstairs\' notes',
|
||||
'isFeedEntry' => True,
|
||||
'note' => $note,
|
||||
'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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
class SecurityController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/login', name: 'login')]
|
||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||
{
|
||||
$error = $authenticationUtils->getLastAuthenticationError();
|
||||
$lastUsername = $authenticationUtils->getLastUsername();
|
||||
|
||||
return $this->render('security/login.html.twig', [
|
||||
'title' => 'Log in',
|
||||
'description' => 'Log in to Joe Carstairs\' personal website',
|
||||
'lastUsername' => $lastUsername,
|
||||
'error' => $error,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/logout', name: 'logout')]
|
||||
public function logout(): void
|
||||
{
|
||||
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Interface\FeedEntry;
|
||||
use App\Repository\BlogPostRepository;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: BlogPostRepository::class)]
|
||||
class BlogPost implements FeedEntry
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $slug = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_MUTABLE)]
|
||||
private ?\DateTime $publishedDate = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_MUTABLE, nullable: true)]
|
||||
private ?\DateTime $updatedDate = null;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
private ?string $content = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $title = null;
|
||||
|
||||
#[ORM\Column(length: 1024)]
|
||||
private ?string $description = null;
|
||||
|
||||
public function getId(): ?int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getSlug(): ?string {
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
public function setSlug(string $slug): static {
|
||||
$this->slug = $slug;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPublishedDate(): ?\DateTime {
|
||||
return $this->publishedDate;
|
||||
}
|
||||
|
||||
public function setPublishedDate(\DateTime $publishedDate): static {
|
||||
$this->publishedDate = $publishedDate;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUpdatedDate(): ?\DateTime {
|
||||
return $this->updatedDate;
|
||||
}
|
||||
|
||||
public function setUpdatedDate(?\DateTime $updatedDate): static {
|
||||
$this->updatedDate = $updatedDate;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContent(): ?string {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function setContent(string $content): static {
|
||||
$this->content = $content;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string {
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): static {
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string {
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(string $description): static {
|
||||
$this->description = $description;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Interface\FeedEntry;
|
||||
use App\Repository\NoteRepository;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: NoteRepository::class)]
|
||||
class Note implements FeedEntry
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $content = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_MUTABLE)]
|
||||
private ?\DateTime $publishedDate = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $slug = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getContent(): ?string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function setContent(string $content): static
|
||||
{
|
||||
$this->content = $content;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPublishedDate(): ?\DateTime
|
||||
{
|
||||
return $this->publishedDate;
|
||||
}
|
||||
|
||||
public function setPublishedDate(\DateTime $publishedDate): static
|
||||
{
|
||||
$this->publishedDate = $publishedDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSlug(): ?string
|
||||
{
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
public function setSlug(string $slug): static
|
||||
{
|
||||
$this->slug = $slug;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace App\Form\Type;
|
||||
|
||||
use App\Entity\Note;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class NoteType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('content', TextType::class)
|
||||
->add('post', SubmitType::class)
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Note::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Interface;
|
||||
|
||||
interface FeedEntry {
|
||||
function getPublishedDate(): ?\DateTime;
|
||||
function getSlug(): ?string;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||
|
||||
class Kernel extends BaseKernel
|
||||
{
|
||||
use MicroKernelTrait;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\BlogPost;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<BlogPost>
|
||||
*/
|
||||
class BlogPostRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry) {
|
||||
parent::__construct($registry, BlogPost::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BlogPost[]
|
||||
*/
|
||||
public function findAllOrderedBySlugDesc(): array
|
||||
{
|
||||
return $this->createQueryBuilder('n')
|
||||
->orderBy('n.slug', 'DESC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Note;
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Note>
|
||||
*/
|
||||
class NoteRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Note::class);
|
||||
}
|
||||
|
||||
public function countWherePublishedOnDate(DateTime $date): int {
|
||||
$dateStr = substr($date->format('c'), 0, 10);
|
||||
|
||||
$wherePublishedOnDate = $this->createQueryBuilder('n')
|
||||
->andWhere('n.publishedDate = :date')
|
||||
->setParameter('date', $dateStr)
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
return count($wherePublishedOnDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Note[] Returns an array of Note objects
|
||||
*/
|
||||
public function findAllOrderedBySlugDesc(): array
|
||||
{
|
||||
return $this->createQueryBuilder('n')
|
||||
->orderBy('n.slug', 'DESC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
{
|
||||
"doctrine/doctrine-bundle": {
|
||||
"version": "2.14",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.13",
|
||||
"ref": "620b57f496f2e599a6015a9fa222c2ee0a32adcb"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/doctrine.yaml",
|
||||
"src/Entity/.gitignore",
|
||||
"src/Repository/.gitignore"
|
||||
]
|
||||
},
|
||||
"doctrine/doctrine-migrations-bundle": {
|
||||
"version": "3.4",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "3.1",
|
||||
"ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/doctrine_migrations.yaml",
|
||||
"migrations/.gitignore"
|
||||
]
|
||||
},
|
||||
"phpunit/phpunit": {
|
||||
"version": "9.6",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "9.6",
|
||||
"ref": "6a9341aa97d441627f8bd424ae85dc04c944f8b4"
|
||||
},
|
||||
"files": [
|
||||
".env.test",
|
||||
"phpunit.xml.dist",
|
||||
"tests/bootstrap.php"
|
||||
]
|
||||
},
|
||||
"symfony/asset-mapper": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.4",
|
||||
"ref": "5ad1308aa756d58f999ffbe1540d1189f5d7d14a"
|
||||
},
|
||||
"files": [
|
||||
"assets/app.js",
|
||||
"assets/styles/app.css",
|
||||
"config/packages/asset_mapper.yaml",
|
||||
"importmap.php"
|
||||
]
|
||||
},
|
||||
"symfony/console": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "5.3",
|
||||
"ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461"
|
||||
},
|
||||
"files": [
|
||||
"bin/console"
|
||||
]
|
||||
},
|
||||
"symfony/debug-bundle": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "5.3",
|
||||
"ref": "5aa8aa48234c8eb6dbdd7b3cd5d791485d2cec4b"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/debug.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/flex": {
|
||||
"version": "2.5",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.4",
|
||||
"ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
|
||||
},
|
||||
"files": [
|
||||
".env",
|
||||
".env.dev"
|
||||
]
|
||||
},
|
||||
"symfony/form": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.2",
|
||||
"ref": "7d86a6723f4a623f59e2bf966b6aad2fc461d36b"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/csrf.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/framework-bundle": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.2",
|
||||
"ref": "105b334917611e8c8071a69749916dfff208f262"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/cache.yaml",
|
||||
"config/packages/framework.yaml",
|
||||
"config/preload.php",
|
||||
"config/routes/framework.yaml",
|
||||
"config/services.yaml",
|
||||
"public/index.php",
|
||||
"src/Controller/.gitignore",
|
||||
"src/Kernel.php",
|
||||
".editorconfig"
|
||||
]
|
||||
},
|
||||
"symfony/mailer": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "4.3",
|
||||
"ref": "09051cfde49476e3c12cd3a0e44289ace1c75a4f"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/mailer.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/maker-bundle": {
|
||||
"version": "1.63",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
|
||||
}
|
||||
},
|
||||
"symfony/messenger": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.0",
|
||||
"ref": "ba1ac4e919baba5644d31b57a3284d6ba12d52ee"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/messenger.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/monolog-bundle": {
|
||||
"version": "3.10",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "3.7",
|
||||
"ref": "aff23899c4440dd995907613c1dd709b6f59503f"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/monolog.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/notifier": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "5.0",
|
||||
"ref": "178877daf79d2dbd62129dd03612cb1a2cb407cc"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/notifier.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/phpunit-bridge": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.3",
|
||||
"ref": "a411a0480041243d97382cac7984f7dce7813c08"
|
||||
},
|
||||
"files": [
|
||||
".env.test",
|
||||
"bin/phpunit",
|
||||
"phpunit.xml.dist",
|
||||
"tests/bootstrap.php"
|
||||
]
|
||||
},
|
||||
"symfony/routing": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.0",
|
||||
"ref": "21b72649d5622d8f7da329ffb5afb232a023619d"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/routing.yaml",
|
||||
"config/routes.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/security-bundle": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.4",
|
||||
"ref": "2ae08430db28c8eb4476605894296c82a642028f"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/security.yaml",
|
||||
"config/routes/security.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/stimulus-bundle": {
|
||||
"version": "2.24",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.20",
|
||||
"ref": "3acc494b566816514a6873a89023a35440b6386d"
|
||||
},
|
||||
"files": [
|
||||
"assets/bootstrap.js",
|
||||
"assets/controllers.json",
|
||||
"assets/controllers/csrf_protection_controller.js",
|
||||
"assets/controllers/hello_controller.js"
|
||||
]
|
||||
},
|
||||
"symfony/translation": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.3",
|
||||
"ref": "e28e27f53663cc34f0be2837aba18e3a1bef8e7b"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/translation.yaml",
|
||||
"translations/.gitignore"
|
||||
]
|
||||
},
|
||||
"symfony/twig-bundle": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.4",
|
||||
"ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/twig.yaml",
|
||||
"templates/base.html.twig"
|
||||
]
|
||||
},
|
||||
"symfony/ux-turbo": {
|
||||
"version": "2.24",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.20",
|
||||
"ref": "c85ff94da66841d7ff087c19cbcd97a2df744ef9"
|
||||
}
|
||||
},
|
||||
"symfony/validator": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.0",
|
||||
"ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/validator.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/web-profiler-bundle": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.1",
|
||||
"ref": "8b51135b84f4266e3b4c8a6dc23c9d1e32e543b7"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/web_profiler.yaml",
|
||||
"config/routes/web_profiler.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/webapp-pack": {
|
||||
"version": "1.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "7d5c5e282f7e2c36a2c3bbb1504f78456c352407"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/messenger.yaml"
|
||||
]
|
||||
},
|
||||
"twig/extra-bundle": {
|
||||
"version": "v3.21.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-GB">
|
||||
<head>
|
||||
{{ include('components/head/_meta.html.twig') }}
|
||||
{{ include('components/head/_indie_web.html.twig') }}
|
||||
{{ include('components/head/_stylesheets.html.twig') }}
|
||||
{{ include('components/head/_feeds.html.twig') }}
|
||||
{{ include('components/head/_open_graph.html.twig') }}
|
||||
{{ include('components/head/_twitter.html.twig') }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{ include('components/_navbar.html.twig') }}
|
||||
|
||||
{% block main %}
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,13 @@
|
||||
<nav class="navbar">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ path('blog_posts') }}">Blog</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ path('notes') }}">Notes</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -0,0 +1,2 @@
|
||||
<link rel="alternate" type="text/xml" title="Blog RSS" href="/blog/rss.xml">
|
||||
<link rel="alternate" type="text/xml" title="Links RSS" href="/links/rss.xml">
|
||||
@@ -0,0 +1,3 @@
|
||||
<link rel="authorization_endpoint" href="https://indieauth.com/auth">
|
||||
<link rel="token_endpoint" href="https://tokens.indieauth.com/token">
|
||||
<link rel="micropub" href="https://tasty-windows-lick.loca.lt">
|
||||
@@ -0,0 +1,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="canonical" href="https://joeac.net{{ path(app.current_route, app.current_route_parameters) }}" />
|
||||
<title>{{ title }}</title>
|
||||
<meta name="title" content="{{ title }}" />
|
||||
<meta name="description" content="{{ description }}" />
|
||||
@@ -0,0 +1,5 @@
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://joeac.net{{ path(app.current_route, app.current_route_parameters) }}" />
|
||||
<meta property="og:title" content="{{ title }}" />
|
||||
<meta property="og:description" content="{{ description }}" />
|
||||
<meta property="og:image" content="/images/headshot.webp" />
|
||||
@@ -0,0 +1,9 @@
|
||||
<link rel="stylesheet" href="/css/reset.css" />
|
||||
<link rel="stylesheet" href="/css/base.css" />
|
||||
<link rel="stylesheet" href="/css/hcard.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 %}
|
||||
@@ -0,0 +1,5 @@
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content="https://joeac.net{{ path(app.current_route, app.current_route_parameters) }}" />
|
||||
<meta property="twitter:title" content="{{ title }}" />
|
||||
<meta property="twitter:description" content="{{ description }}" />
|
||||
<meta property="twitter:image" content="/images/headshot.webp" />
|
||||
@@ -0,0 +1,54 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<section class="h-card">
|
||||
<div>
|
||||
<img class="u-photo" src="/images/headshot.webp" height="96" width="96" />
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<h1>
|
||||
Joe Carstairs
|
||||
</h1>
|
||||
|
||||
<div hidden>
|
||||
<a class="p-name u-url u-uid" href="https://joeac.net" rel="me">
|
||||
Permalink
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="h-card__text">
|
||||
<p>
|
||||
Hi! 👋 My name is <span class="p-given-name">Joe</span>
|
||||
<span class="p-family-name">Carstairs</span>. I’m a
|
||||
<span class="p-job-title">software developer</span> at
|
||||
<a class="p-org" href="https://www.scottlogic.com">Scott Logic</a>, a
|
||||
graduate of Philosophy and Mathematics at the University of Edinburgh,
|
||||
a committed Christian and a pretty rubbish poet.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
I’m also the <span class="p-job-title">secretary</span> of the
|
||||
<a class="p-org" href="https://scotsleidassocie.org">Scots Language Society</a>.
|
||||
<a href="https://github.com/joeacarstairs/lallans-wabsteid-astro">Help me maintain our website!</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
HMU with your thoughts on philosophy of science, Scots verse and
|
||||
John the Evangelist.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<small>
|
||||
Or get me on
|
||||
<a href="https://www.facebook.com/joe.carstairs.5" rel="me">Facebook</a>,
|
||||
<a href="https://mastodon.social/@joe_carstairs" rel="me">Mastodon</a>,
|
||||
<a href="https://www.linkedin.com/in/joe-carstairs-0aa936277" rel="me">LinkedIn</a>,
|
||||
<a href="https://bsky.app/profile/joeacarstairs.bsky.social" rel="me">BlueSky</a>,
|
||||
or <a href="https://github.com/joeacarstairs" rel="me">GitHub</a>.
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block main %}
|
||||
{% if note %}
|
||||
<article class="h-entry">
|
||||
<header>
|
||||
<h1 class="p-name">Note {{ note.slug }}</h1>
|
||||
<p hidden><a class="p-author h-card" href="{{ url('index') }}">Joe Carstairs</a></p>
|
||||
<p hidden <a class="u-url" href="{{ url('note', { slug: note.slug }) }}">Permalink</a></p>
|
||||
<p>Added: <time class="dt-published">{{ note.publishedDate.format('c') }}</time></p>
|
||||
</header>
|
||||
|
||||
<section class="e-content">
|
||||
{{ note.content|markdown_to_html }}
|
||||
</section>
|
||||
</article>
|
||||
{% else %}
|
||||
<main>
|
||||
<header>
|
||||
<h1>Note not found</h1>
|
||||
</header>
|
||||
<section>
|
||||
<p>I don't have a note '{{ slug }}'.</p>
|
||||
<p>Go back to <a href="/notes">Notes</a>.</p>
|
||||
</section>
|
||||
</main>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,71 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<section class="h-feed">
|
||||
<h1 class="p-name">Joe Carstairs' notes</h1>
|
||||
|
||||
<p hidden>
|
||||
These links are collected by
|
||||
<a class="p-author h-card" href="{{ url('index') }}">
|
||||
Joe Carstairs
|
||||
</a>.
|
||||
</p>
|
||||
<p hidden>
|
||||
<a class="u-url" href="{{ url('notes') }}">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 note in notes[year][month] %}
|
||||
<section class="h-entry">
|
||||
<a class="u-url" href="{{ url('note', { slug: note.slug }) }}">
|
||||
<h4 class="p-name">
|
||||
Note {{ note.slug }}
|
||||
</h4>
|
||||
<p>
|
||||
Added:
|
||||
<time class="dt-published" datetime="{{ note.publishedDate.format('c') }}">
|
||||
{{ note.publishedDate.format('j F Y') }}
|
||||
</time>
|
||||
</p>
|
||||
|
||||
<section class="e-content">
|
||||
{% apply markdown_to_html %}
|
||||
{{ note.content|markdown_to_html|striptags('<i><em><b><strong><sup><p>')|html_to_markdown }}
|
||||
{% endapply %}
|
||||
</section>
|
||||
</a>
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>I have no notes.</p>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,47 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<h1>Log in</h1>
|
||||
|
||||
{% if app.user %}
|
||||
<p>
|
||||
You are logged in as {{ app.user.userIdentifier }}.
|
||||
If you're not happy about that, <a href="{{ path('logout') }}">log out</a>.
|
||||
</p>
|
||||
{% else %}
|
||||
{% if error %}
|
||||
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="{{ path('login') }}" method="POST">
|
||||
<label for="username">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value="{{ lastUsername }}"
|
||||
name="_username"
|
||||
id="username"
|
||||
class="form-control"
|
||||
autocomplete="email"
|
||||
required
|
||||
autofocus
|
||||
>
|
||||
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
name="_password"
|
||||
id="password"
|
||||
class="form-control"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
>
|
||||
|
||||
<input type="hidden" name="_target_path" value="{{ path('index') }}">
|
||||
<input type="hidden" name="_csrf_token" data-controller="csrf-protection" value="{{ csrf_token('authenticate') }}">
|
||||
|
||||
<button type="submit">Log in</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<h1>Write a note</h1>
|
||||
{{ form(form) }}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
|
||||
require dirname(__DIR__).'/vendor/autoload.php';
|
||||
|
||||
if (method_exists(Dotenv::class, 'bootEnv')) {
|
||||
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
|
||||
}
|
||||
Reference in New Issue
Block a user