Move symfony stuff to own folder
This commit is contained in:
41
symfony/.env
Normal file
41
symfony/.env
Normal file
@@ -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 ###
|
||||
4
symfony/.env.dev
Normal file
4
symfony/.env.dev
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_SECRET=59e8f6a1ec4b7c0d4c3126975ed96a64
|
||||
###< symfony/framework-bundle ###
|
||||
6
symfony/.env.test
Normal file
6
symfony/.env.test
Normal file
@@ -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
|
||||
24
symfony/.gitignore
vendored
Normal file
24
symfony/.gitignore
vendored
Normal file
@@ -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 ###
|
||||
10
symfony/assets/app.js
Normal file
10
symfony/assets/app.js
Normal file
@@ -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! 🎉');
|
||||
5
symfony/assets/bootstrap.js
vendored
Normal file
5
symfony/assets/bootstrap.js
vendored
Normal file
@@ -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);
|
||||
15
symfony/assets/controllers.json
Normal file
15
symfony/assets/controllers.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"controllers": {
|
||||
"@symfony/ux-turbo": {
|
||||
"turbo-core": {
|
||||
"enabled": true,
|
||||
"fetch": "eager"
|
||||
},
|
||||
"mercure-turbo-stream": {
|
||||
"enabled": false,
|
||||
"fetch": "eager"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoints": []
|
||||
}
|
||||
79
symfony/assets/controllers/csrf_protection_controller.js
Normal file
79
symfony/assets/controllers/csrf_protection_controller.js
Normal file
@@ -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';
|
||||
16
symfony/assets/controllers/hello_controller.js
Normal file
16
symfony/assets/controllers/hello_controller.js
Normal file
@@ -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';
|
||||
}
|
||||
}
|
||||
3
symfony/assets/styles/app.css
Normal file
3
symfony/assets/styles/app.css
Normal file
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
background-color: skyblue;
|
||||
}
|
||||
21
symfony/bin/console
Executable file
21
symfony/bin/console
Executable file
@@ -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);
|
||||
};
|
||||
23
symfony/bin/phpunit
Executable file
23
symfony/bin/phpunit
Executable file
@@ -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';
|
||||
}
|
||||
112
symfony/composer.json
Normal file
112
symfony/composer.json
Normal file
@@ -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.*"
|
||||
}
|
||||
}
|
||||
10506
symfony/composer.lock
generated
Normal file
10506
symfony/composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
symfony/config/bundles.php
Normal file
16
symfony/config/bundles.php
Normal file
@@ -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],
|
||||
];
|
||||
11
symfony/config/packages/asset_mapper.yaml
Normal file
11
symfony/config/packages/asset_mapper.yaml
Normal file
@@ -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
|
||||
19
symfony/config/packages/cache.yaml
Normal file
19
symfony/config/packages/cache.yaml
Normal file
@@ -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
|
||||
11
symfony/config/packages/csrf.yaml
Normal file
11
symfony/config/packages/csrf.yaml
Normal file
@@ -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
|
||||
5
symfony/config/packages/debug.yaml
Normal file
5
symfony/config/packages/debug.yaml
Normal file
@@ -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)%"
|
||||
54
symfony/config/packages/doctrine.yaml
Normal file
54
symfony/config/packages/doctrine.yaml
Normal file
@@ -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
|
||||
6
symfony/config/packages/doctrine_migrations.yaml
Normal file
6
symfony/config/packages/doctrine_migrations.yaml
Normal file
@@ -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
|
||||
15
symfony/config/packages/framework.yaml
Normal file
15
symfony/config/packages/framework.yaml
Normal file
@@ -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
|
||||
3
symfony/config/packages/mailer.yaml
Normal file
3
symfony/config/packages/mailer.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
framework:
|
||||
mailer:
|
||||
dsn: '%env(MAILER_DSN)%'
|
||||
29
symfony/config/packages/messenger.yaml
Normal file
29
symfony/config/packages/messenger.yaml
Normal file
@@ -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
|
||||
62
symfony/config/packages/monolog.yaml
Normal file
62
symfony/config/packages/monolog.yaml
Normal file
@@ -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
|
||||
12
symfony/config/packages/notifier.yaml
Normal file
12
symfony/config/packages/notifier.yaml
Normal file
@@ -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 }
|
||||
10
symfony/config/packages/routing.yaml
Normal file
10
symfony/config/packages/routing.yaml
Normal file
@@ -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
|
||||
32
symfony/config/packages/security.yaml
Normal file
32
symfony/config/packages/security.yaml
Normal file
@@ -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
|
||||
7
symfony/config/packages/translation.yaml
Normal file
7
symfony/config/packages/translation.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
framework:
|
||||
default_locale: en
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
fallbacks:
|
||||
- en
|
||||
providers:
|
||||
6
symfony/config/packages/twig.yaml
Normal file
6
symfony/config/packages/twig.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
twig:
|
||||
file_name_pattern: '*.twig'
|
||||
|
||||
when@test:
|
||||
twig:
|
||||
strict_variables: true
|
||||
11
symfony/config/packages/validator.yaml
Normal file
11
symfony/config/packages/validator.yaml
Normal file
@@ -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
|
||||
11
symfony/config/packages/web_profiler.yaml
Normal file
11
symfony/config/packages/web_profiler.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
when@dev:
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
|
||||
framework:
|
||||
profiler:
|
||||
collect_serializer_data: true
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
profiler: { collect: false }
|
||||
5
symfony/config/preload.php
Normal file
5
symfony/config/preload.php
Normal file
@@ -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';
|
||||
}
|
||||
5
symfony/config/routes.yaml
Normal file
5
symfony/config/routes.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
controllers:
|
||||
resource:
|
||||
path: ../src/Controller/
|
||||
namespace: App\Controller
|
||||
type: attribute
|
||||
4
symfony/config/routes/framework.yaml
Normal file
4
symfony/config/routes/framework.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
when@dev:
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /_error
|
||||
3
symfony/config/routes/security.yaml
Normal file
3
symfony/config/routes/security.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
_security_logout:
|
||||
resource: security.route_loader.logout
|
||||
type: service
|
||||
8
symfony/config/routes/web_profiler.yaml
Normal file
8
symfony/config/routes/web_profiler.yaml
Normal file
@@ -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
|
||||
25
symfony/config/services.yaml
Normal file
25
symfony/config/services.yaml
Normal file
@@ -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
|
||||
28
symfony/importmap.php
Normal file
28
symfony/importmap.php
Normal file
@@ -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
symfony/migrations/.gitignore
vendored
Normal file
0
symfony/migrations/.gitignore
vendored
Normal file
53
symfony/migrations/Version20250517200832.php
Normal file
53
symfony/migrations/Version20250517200832.php
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
47
symfony/migrations/Version20250517212750.php
Normal file
47
symfony/migrations/Version20250517212750.php
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
18
symfony/migrations/Version20250519215136.php
Normal file
18
symfony/migrations/Version20250519215136.php
Normal file
@@ -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 {}
|
||||
}
|
||||
73
symfony/migrations/Version20250521000000.php
Normal file
73
symfony/migrations/Version20250521000000.php
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
35
symfony/migrations/Version20250522212213.php
Normal file
35
symfony/migrations/Version20250522212213.php
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
68
symfony/migrations/Version20250522212300.php
Normal file
68
symfony/migrations/Version20250522212300.php
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
symfony/phpunit.xml.dist
Normal file
38
symfony/phpunit.xml.dist
Normal file
@@ -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>
|
||||
316
symfony/public/css/base.css
Normal file
316
symfony/public/css/base.css
Normal file
@@ -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;
|
||||
}
|
||||
86
symfony/public/css/cv.css
Normal file
86
symfony/public/css/cv.css
Normal file
@@ -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);
|
||||
}
|
||||
14
symfony/public/css/feed-entry.css
Normal file
14
symfony/public/css/feed-entry.css
Normal file
@@ -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);
|
||||
}
|
||||
76
symfony/public/css/feed.css
Normal file
76
symfony/public/css/feed.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
57
symfony/public/css/hcard.css
Normal file
57
symfony/public/css/hcard.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
73
symfony/public/css/reset.css
Normal file
73
symfony/public/css/reset.css
Normal file
@@ -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 |
BIN
symfony/public/images/blog/2024/06/30/ncuti-gatwa-promo-pic.webp
Normal file
BIN
symfony/public/images/blog/2024/06/30/ncuti-gatwa-promo-pic.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
BIN
symfony/public/images/headshot.webp
Normal file
BIN
symfony/public/images/headshot.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
symfony/public/images/headshot_large.jpg
Normal file
BIN
symfony/public/images/headshot_large.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 949 KiB |
9
symfony/public/index.php
Normal file
9
symfony/public/index.php
Normal file
@@ -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
symfony/src/Controller/.gitignore
vendored
Normal file
0
symfony/src/Controller/.gitignore
vendored
Normal file
257
symfony/src/Controller/GuiController.php
Normal file
257
symfony/src/Controller/GuiController.php
Normal file
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
31
symfony/src/Controller/SecurityController.php
Normal file
31
symfony/src/Controller/SecurityController.php
Normal file
@@ -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
symfony/src/Entity/.gitignore
vendored
Normal file
0
symfony/src/Entity/.gitignore
vendored
Normal file
93
symfony/src/Entity/BlogPost.php
Normal file
93
symfony/src/Entity/BlogPost.php
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
67
symfony/src/Entity/Note.php
Normal file
67
symfony/src/Entity/Note.php
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
27
symfony/src/Form/Type/NoteType.php
Normal file
27
symfony/src/Form/Type/NoteType.php
Normal file
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
8
symfony/src/Interface/FeedEntry.php
Normal file
8
symfony/src/Interface/FeedEntry.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Interface;
|
||||
|
||||
interface FeedEntry {
|
||||
function getPublishedDate(): ?\DateTime;
|
||||
function getSlug(): ?string;
|
||||
}
|
||||
11
symfony/src/Kernel.php
Normal file
11
symfony/src/Kernel.php
Normal file
@@ -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
symfony/src/Repository/.gitignore
vendored
Normal file
0
symfony/src/Repository/.gitignore
vendored
Normal file
28
symfony/src/Repository/BlogPostRepository.php
Normal file
28
symfony/src/Repository/BlogPostRepository.php
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
43
symfony/src/Repository/NoteRepository.php
Normal file
43
symfony/src/Repository/NoteRepository.php
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
315
symfony/symfony.lock
Normal file
315
symfony/symfony.lock
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
21
symfony/templates/base.html.twig
Normal file
21
symfony/templates/base.html.twig
Normal file
@@ -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>
|
||||
47
symfony/templates/blog_post.html.twig
Normal file
47
symfony/templates/blog_post.html.twig
Normal file
@@ -0,0 +1,47 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
{% if post %}
|
||||
<article class="h-entry">
|
||||
<aside>
|
||||
<span>
|
||||
This is a blog post by
|
||||
<a class="p-author h-card" href="/">Joe Carstairs</a>.
|
||||
</span>
|
||||
<p>
|
||||
Published: <time class="dt-published">{{ post.publishedDate.format('c') }}
|
||||
</p>
|
||||
{% if post.updatedDate %}}
|
||||
<p>
|
||||
Updated: <time class="dt-updated">{{ post.updatedDate.format('c') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<span hidden>
|
||||
<a class="u-url uid" href="{{ url('blog_post', { slug: post.slug }) }}">
|
||||
Permalink
|
||||
</a>
|
||||
</span>
|
||||
</aside>
|
||||
|
||||
<header>
|
||||
<h1 class="p-name">{% apply markdown_to_html %}{{ post.title }}{% endapply %}</h1>
|
||||
<p class="p-summary">
|
||||
{% apply markdown_to_html %}{{ post.description }}{% endapply %}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
|
||||
{% apply markdown_to_html %}
|
||||
<section class="e-content">
|
||||
{{ post.content }}
|
||||
</section>
|
||||
{% endapply %}
|
||||
</article>
|
||||
{% else %}
|
||||
<section>
|
||||
<h1>Post not found</h1>
|
||||
<p>I don't have a blog post '{{ slug }}'.</p>
|
||||
<p>Go back to <a href="{{ path('blog_posts') }}">Blog</a>.</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
81
symfony/templates/blog_posts.html.twig
Normal file
81
symfony/templates/blog_posts.html.twig
Normal file
@@ -0,0 +1,81 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<section class="h-feed">
|
||||
<h1 class="p-name">Joe Carstairs' blog</h1>
|
||||
|
||||
<p hidden>
|
||||
This blog is written by
|
||||
<a class="p-author h-card" href="{{ url('index') }}">
|
||||
Joe Carstairs
|
||||
</a>.
|
||||
</p>
|
||||
<p hidden>
|
||||
<a class="u-url" href="{{ url('blog_posts') }}">Permalink</a>
|
||||
</p>
|
||||
|
||||
{% if years %}
|
||||
<nav class="skip-to">
|
||||
Skip to:
|
||||
<ul>
|
||||
{% for year in years %}
|
||||
<li><a href="#{{ year }}">{{ year }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% for year in years %}
|
||||
<h2 id="{{ year }}">{{ year }}</h2>
|
||||
|
||||
<nav class="skip-to">
|
||||
Skip to:
|
||||
<ul>
|
||||
{% for month in months[year] %}
|
||||
<li><a href="#{{ year }}-{{ month }}">
|
||||
{{ monthNames[month] }} <span class="visually-hidden">{{ year }}</span>
|
||||
</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{% for month in months[year] %}
|
||||
<h3 id="{{ year }}-{{ month }}">{{ monthNames[month] }}</h3>
|
||||
|
||||
{% for post in posts[year][month] %}
|
||||
<section class="h-entry">
|
||||
<a class="u-url" href="{{ path('blog_post', { 'slug': post.slug }) }}">
|
||||
<h4 class="p-name">
|
||||
{{ post.title }}
|
||||
</h4>
|
||||
|
||||
<p>
|
||||
Added:
|
||||
<time class="dt-published" datetime="{{ post.publishedDate.format('c') }}">
|
||||
{{ post.publishedDate.format('j F Y') }}
|
||||
</time>
|
||||
</p>
|
||||
|
||||
{% if post.updatedDate %}
|
||||
<p>
|
||||
Updated:
|
||||
<time class="dt-updated" datetime="{{ post.updatedDate.format('c') }}">
|
||||
{{ post.updatedDate.format('j F Y') }}
|
||||
</time>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<section class="p-summary">
|
||||
{% apply markdown_to_html %}
|
||||
{{ post.description|markdown_to_html|striptags('<i><em><b><strong><sup><p>')|html_to_markdown }}
|
||||
{% endapply %}
|
||||
</section>
|
||||
</a>
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>I have no blog posts.</p>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
13
symfony/templates/components/_navbar.html.twig
Normal file
13
symfony/templates/components/_navbar.html.twig
Normal file
@@ -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>
|
||||
2
symfony/templates/components/head/_feeds.html.twig
Normal file
2
symfony/templates/components/head/_feeds.html.twig
Normal file
@@ -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">
|
||||
3
symfony/templates/components/head/_indie_web.html.twig
Normal file
3
symfony/templates/components/head/_indie_web.html.twig
Normal file
@@ -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">
|
||||
7
symfony/templates/components/head/_meta.html.twig
Normal file
7
symfony/templates/components/head/_meta.html.twig
Normal file
@@ -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 }}" />
|
||||
5
symfony/templates/components/head/_open_graph.html.twig
Normal file
5
symfony/templates/components/head/_open_graph.html.twig
Normal file
@@ -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" />
|
||||
9
symfony/templates/components/head/_stylesheets.html.twig
Normal file
9
symfony/templates/components/head/_stylesheets.html.twig
Normal file
@@ -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 %}
|
||||
5
symfony/templates/components/head/_twitter.html.twig
Normal file
5
symfony/templates/components/head/_twitter.html.twig
Normal file
@@ -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" />
|
||||
54
symfony/templates/index.html.twig
Normal file
54
symfony/templates/index.html.twig
Normal file
@@ -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 %}
|
||||
28
symfony/templates/note.html.twig
Normal file
28
symfony/templates/note.html.twig
Normal file
@@ -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 %}
|
||||
71
symfony/templates/notes.html.twig
Normal file
71
symfony/templates/notes.html.twig
Normal file
@@ -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 %}
|
||||
47
symfony/templates/security/login.html.twig
Normal file
47
symfony/templates/security/login.html.twig
Normal file
@@ -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 %}
|
||||
8
symfony/templates/write_note.html.twig
Normal file
8
symfony/templates/write_note.html.twig
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<h1>Write a note</h1>
|
||||
{{ form(form) }}
|
||||
</section>
|
||||
{% endblock %}
|
||||
9
symfony/tests/bootstrap.php
Normal file
9
symfony/tests/bootstrap.php
Normal file
@@ -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');
|
||||
}
|
||||
0
symfony/translations/.gitignore
vendored
Normal file
0
symfony/translations/.gitignore
vendored
Normal file
Reference in New Issue
Block a user