Merge pull request #1188 from jonnybarnes/develop

MTM Add snow
This commit is contained in:
Jonny Barnes 2023-12-17 14:45:57 +00:00 committed by GitHub
commit 6ff247d58b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1471 additions and 858 deletions

View file

@ -18,7 +18,7 @@ class ArticlesController extends Controller
/** /**
* Show all articles (with pagination). * Show all articles (with pagination).
*/ */
public function index(int $year = null, int $month = null): View public function index(?int $year = null, ?int $month = null): View
{ {
$articles = Article::where('published', '1') $articles = Article::where('published', '1')
->date($year, $month) ->date($year, $month)

View file

@ -18,7 +18,7 @@ use Illuminate\Http\UploadedFile;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Intervention\Image\Exception\NotReadableException; use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\ImageManager; use Intervention\Image\ImageManager;
use Lcobucci\JWT\Token\InvalidTokenStructure; use Lcobucci\JWT\Token\InvalidTokenStructure;
use Lcobucci\JWT\Validation\RequiredConstraintsViolated; use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
@ -108,7 +108,7 @@ class MicropubMediaController extends Controller
{ {
try { try {
$tokenData = $this->tokenService->validateToken($request->input('access_token')); $tokenData = $this->tokenService->validateToken($request->input('access_token'));
} catch (RequiredConstraintsViolated|InvalidTokenStructure $exception) { } catch (RequiredConstraintsViolated|InvalidTokenStructure) {
$micropubResponses = new MicropubResponses(); $micropubResponses = new MicropubResponses();
return $micropubResponses->invalidTokenResponse(); return $micropubResponses->invalidTokenResponse();
@ -144,11 +144,12 @@ class MicropubMediaController extends Controller
$filename = $this->saveFile($request->file('file')); $filename = $this->saveFile($request->file('file'));
/** @var ImageManager $manager */
$manager = resolve(ImageManager::class); $manager = resolve(ImageManager::class);
try { try {
$image = $manager->make($request->file('file')); $image = $manager->read($request->file('file'));
$width = $image->width(); $width = $image->width();
} catch (NotReadableException $exception) { } catch (DecoderException) {
// not an image // not an image
$width = null; $width = null;
} }

View file

@ -10,7 +10,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Intervention\Image\Exception\NotReadableException; use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\ImageManager; use Intervention\Image\ImageManager;
class ProcessMedia implements ShouldQueue class ProcessMedia implements ShouldQueue
@ -35,8 +35,8 @@ class ProcessMedia implements ShouldQueue
{ {
//open file //open file
try { try {
$image = $manager->make(storage_path('app') . '/' . $this->filename); $image = $manager->read(storage_path('app') . '/' . $this->filename);
} catch (NotReadableException $exception) { } catch (DecoderException) {
// not an image; delete file and end job // not an image; delete file and end job
unlink(storage_path('app') . '/' . $this->filename); unlink(storage_path('app') . '/' . $this->filename);

View file

@ -107,7 +107,7 @@ class Article extends Model
/** /**
* Scope a query to only include articles from a particular year/month. * Scope a query to only include articles from a particular year/month.
*/ */
public function scopeDate(Builder $query, int $year = null, int $month = null): Builder public function scopeDate(Builder $query, ?int $year = null, ?int $month = null): Builder
{ {
if ($year === null) { if ($year === null) {
return $query; return $query;

View file

@ -30,7 +30,7 @@ class AppServiceProvider extends ServiceProvider
// configure Intervention/Image // configure Intervention/Image
$this->app->bind('Intervention\Image\ImageManager', function () { $this->app->bind('Intervention\Image\ImageManager', function () {
return new \Intervention\Image\ImageManager(['driver' => config('image.driver')]); return \Intervention\Image\ImageManager::withDriver(config('image.driver'));
}); });
// Bind the Codebird client // Bind the Codebird client

View file

@ -8,7 +8,7 @@ use App\Models\Article;
class ArticleService extends Service class ArticleService extends Service
{ {
public function create(array $request, string $client = null): Article public function create(array $request, ?string $client = null): Article
{ {
return Article::create([ return Article::create([
'title' => $this->getDataByKey($request, 'name'), 'title' => $this->getDataByKey($request, 'name'),

View file

@ -18,7 +18,7 @@ class BookmarkService extends Service
/** /**
* Create a new Bookmark. * Create a new Bookmark.
*/ */
public function create(array $request, string $client = null): Bookmark public function create(array $request, ?string $client = null): Bookmark
{ {
if (Arr::get($request, 'properties.bookmark-of.0')) { if (Arr::get($request, 'properties.bookmark-of.0')) {
//micropub request //micropub request

View file

@ -13,7 +13,7 @@ class LikeService extends Service
/** /**
* Create a new Like. * Create a new Like.
*/ */
public function create(array $request, string $client = null): Like public function create(array $request, ?string $client = null): Like
{ {
if (Arr::get($request, 'properties.like-of.0')) { if (Arr::get($request, 'properties.like-of.0')) {
//micropub request //micropub request

View file

@ -15,7 +15,7 @@ class HEntryService
/** /**
* Create the relevant model from some h-entry data. * Create the relevant model from some h-entry data.
*/ */
public function process(array $request, string $client = null): ?string public function process(array $request, ?string $client = null): ?string
{ {
if (Arr::get($request, 'properties.like-of') || Arr::get($request, 'like-of')) { if (Arr::get($request, 'properties.like-of') || Arr::get($request, 'like-of')) {
return resolve(LikeService::class)->create($request)->longurl; return resolve(LikeService::class)->create($request)->longurl;

View file

@ -18,7 +18,7 @@ class NoteService extends Service
/** /**
* Create a new note. * Create a new note.
*/ */
public function create(array $request, string $client = null): Note public function create(array $request, ?string $client = null): Note
{ {
$note = Note::create( $note = Note::create(
[ [

View file

@ -9,7 +9,7 @@ use Illuminate\Support\Arr;
abstract class Service abstract class Service
{ {
abstract public function create(array $request, string $client = null): Model; abstract public function create(array $request, ?string $client = null): Model;
protected function getDataByKey(array $request, string $key): ?string protected function getDataByKey(array $request, string $key): ?string
{ {

View file

@ -13,7 +13,7 @@
"cviebrock/eloquent-sluggable": "^10.0", "cviebrock/eloquent-sluggable": "^10.0",
"guzzlehttp/guzzle": "^7.2", "guzzlehttp/guzzle": "^7.2",
"indieauth/client": "^1.1", "indieauth/client": "^1.1",
"intervention/image": "^2.4", "intervention/image": "^3",
"jonnybarnes/indieweb": "~0.2", "jonnybarnes/indieweb": "~0.2",
"jonnybarnes/webmentions-parser": "~0.5", "jonnybarnes/webmentions-parser": "~0.5",
"jublonet/codebird-php": "4.0.0-beta.1", "jublonet/codebird-php": "4.0.0-beta.1",

713
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,7 @@
<?php <?php
use Intervention\Image\Drivers\Gd\Driver;
return [ return [
/* /*
@ -15,6 +17,6 @@ return [
| |
*/ */
'driver' => 'gd', 'driver' => Driver::class,
]; ];

1092
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,19 +5,19 @@
"repository": "https://github.com/jonnybarnes/jonnybarnes.uk", "repository": "https://github.com/jonnybarnes/jonnybarnes.uk",
"license": "CC0-1.0", "license": "CC0-1.0",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.23.5", "@babel/core": "^7.23.6",
"@babel/preset-env": "^7.23.5", "@babel/preset-env": "^7.23.6",
"@csstools/postcss-oklab-function": "^3.0.7", "@csstools/postcss-oklab-function": "^3.0.7",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
"browserlist": "^1.0.1", "browserlist": "^1.0.1",
"compression-webpack-plugin": "^10.0.0", "compression-webpack-plugin": "^10.0.0",
"css-loader": "^6.8.1", "css-loader": "^6.8.1",
"cssnano": "^6.0.1", "cssnano": "^6.0.2",
"eslint": "^8.54.0", "eslint": "^8.55.0",
"eslint-webpack-plugin": "^4.0.1", "eslint-webpack-plugin": "^4.0.1",
"mini-css-extract-plugin": "^2.7.6", "mini-css-extract-plugin": "^2.7.6",
"postcss": "^8.4.31", "postcss": "^8.4.32",
"postcss-combine-duplicated-selectors": "^10.0.2", "postcss-combine-duplicated-selectors": "^10.0.2",
"postcss-combine-media-query": "^1.0.1", "postcss-combine-media-query": "^1.0.1",
"postcss-import": "^15.1.0", "postcss-import": "^15.1.0",
@ -39,5 +39,9 @@
"> 1%", "> 1%",
"not IE 11", "not IE 11",
"not IE_Mob 11" "not IE_Mob 11"
] ],
"dependencies": {
"@11ty/is-land": "^4.0.0",
"@zachleat/snow-fall": "^1.0.2"
}
} }

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -0,0 +1,338 @@
class Island extends HTMLElement {
static tagName = "is-land";
static prefix = "is-land--";
static attr = {
template: "data-island",
ready: "ready",
defer: "defer-hydration",
};
static onceCache = new Map();
static onReady = new Map();
static fallback = {
":not(is-land,:defined,[defer-hydration])": (readyPromise, node, prefix) => {
// remove from document to prevent web component init
let cloned = document.createElement(prefix + node.localName);
for(let attr of node.getAttributeNames()) {
cloned.setAttribute(attr, node.getAttribute(attr));
}
// Declarative Shadow DOM (with polyfill)
let shadowroot = node.shadowRoot;
if(!shadowroot) {
let tmpl = node.querySelector(":scope > template:is([shadowrootmode], [shadowroot])");
if(tmpl) {
let mode = tmpl.getAttribute("shadowrootmode") || tmpl.getAttribute("shadowroot") || "closed";
shadowroot = node.attachShadow({ mode }); // default is closed
shadowroot.appendChild(tmpl.content.cloneNode(true));
}
}
// Cheers to https://gist.github.com/developit/45c85e9be01e8c3f1a0ec073d600d01e
if(shadowroot) {
cloned.attachShadow({ mode: shadowroot.mode }).append(...shadowroot.childNodes);
}
// Keep *same* child nodes to preserve state of children (e.g. details->summary)
cloned.append(...node.childNodes);
node.replaceWith(cloned);
return readyPromise.then(() => {
// Restore original children and shadow DOM
if(cloned.shadowRoot) {
node.shadowRoot.append(...cloned.shadowRoot.childNodes);
}
node.append(...cloned.childNodes);
cloned.replaceWith(node);
});
}
}
constructor() {
super();
// Internal promises
this.ready = new Promise(resolve => {
this.readyResolve = resolve;
});
}
// any parents of `el` that are <is-land> (with conditions)
static getParents(el, stopAt = false) {
let nodes = [];
while(el) {
if(el.matches && el.matches(Island.tagName)) {
if(stopAt && el === stopAt) {
break;
}
if(Conditions.hasConditions(el)) {
nodes.push(el);
}
}
el = el.parentNode;
}
return nodes;
}
static async ready(el, parents) {
if(!parents) {
parents = Island.getParents(el);
}
if(parents.length === 0) {
return;
}
let imports = await Promise.all(parents.map(p => p.wait()));
// return innermost module import
if(imports.length) {
return imports[0];
}
}
forceFallback() {
if(window.Island) {
Object.assign(Island.fallback, window.Island.fallback);
}
for(let selector in Island.fallback) {
// Reverse here as a cheap way to get the deepest nodes first
let components = Array.from(this.querySelectorAll(selector)).reverse();
// with thanks to https://gist.github.com/cowboy/938767
for(let node of components) {
if(!node.isConnected) {
continue;
}
let parents = Island.getParents(node);
// must be in a leaf island (not nested deep)
if(parents.length === 1) {
let p = Island.ready(node, parents);
Island.fallback[selector](p, node, Island.prefix);
}
}
}
}
wait() {
return this.ready;
}
async connectedCallback() {
// Only use fallback content with loading conditions
if(Conditions.hasConditions(this)) {
// Keep fallback content without initializing the components
this.forceFallback();
}
await this.hydrate();
}
getTemplates() {
return this.querySelectorAll(`template[${Island.attr.template}]`);
}
replaceTemplates(templates) {
// replace <template> with the live content
for(let node of templates) {
// if the template is nested inside another child <is-land> inside, skip
if(Island.getParents(node, this).length > 0) {
continue;
}
let value = node.getAttribute(Island.attr.template);
// get rid of the rest of the content on the island
if(value === "replace") {
let children = Array.from(this.childNodes);
for(let child of children) {
this.removeChild(child);
}
this.appendChild(node.content);
break;
} else {
let html = node.innerHTML;
if(value === "once" && html) {
if(Island.onceCache.has(html)) {
node.remove();
return;
}
Island.onceCache.set(html, true);
}
node.replaceWith(node.content);
}
}
}
async hydrate() {
let conditions = [];
if(this.parentNode) {
// wait for all parents before hydrating
conditions.push(Island.ready(this.parentNode));
}
let attrs = Conditions.getConditions(this);
for(let condition in attrs) {
if(Conditions.map[condition]) {
conditions.push(Conditions.map[condition](attrs[condition], this));
}
}
// Loading conditions must finish before dependencies are loaded
await Promise.all(conditions);
this.replaceTemplates(this.getTemplates());
for(let fn of Island.onReady.values()) {
await fn.call(this, Island);
}
this.readyResolve();
this.setAttribute(Island.attr.ready, "");
// Remove [defer-hydration]
this.querySelectorAll(`[${Island.attr.defer}]`).forEach(node => node.removeAttribute(Island.attr.defer));
}
}
class Conditions {
static map = {
visible: Conditions.visible,
idle: Conditions.idle,
interaction: Conditions.interaction,
media: Conditions.media,
"save-data": Conditions.saveData,
};
static hasConditions(node) {
return Object.keys(Conditions.getConditions(node)).length > 0;
}
static getConditions(node) {
let map = {};
for(let key of Object.keys(Conditions.map)) {
if(node.hasAttribute(`on:${key}`)) {
map[key] = node.getAttribute(`on:${key}`);
}
}
return map;
}
static visible(noop, el) {
if(!('IntersectionObserver' in window)) {
// runs immediately
return;
}
return new Promise(resolve => {
let observer = new IntersectionObserver(entries => {
let [entry] = entries;
if(entry.isIntersecting) {
observer.unobserve(entry.target);
resolve();
}
});
observer.observe(el);
});
}
// Warning: on:idle is not very useful with other conditions as it may resolve long before.
static idle() {
let onload = new Promise(resolve => {
if(document.readyState !== "complete") {
window.addEventListener("load", () => resolve(), { once: true });
} else {
resolve();
}
});
if(!("requestIdleCallback" in window)) {
// run immediately
return onload;
}
// both idle and onload
return Promise.all([
new Promise(resolve => {
requestIdleCallback(() => {
resolve();
});
}),
onload,
]);
}
static interaction(eventOverrides, el) {
let events = ["click", "touchstart"];
// event overrides e.g. on:interaction="mouseenter"
if(eventOverrides) {
events = (eventOverrides || "").split(",").map(entry => entry.trim());
}
return new Promise(resolve => {
function resolveFn(e) {
resolve();
// cleanup the other event handlers
for(let name of events) {
el.removeEventListener(name, resolveFn);
}
}
for(let name of events) {
el.addEventListener(name, resolveFn, { once: true });
}
});
}
static media(query) {
let mm = {
matches: true
};
if(query && ("matchMedia" in window)) {
mm = window.matchMedia(query);
}
if(mm.matches) {
return;
}
return new Promise(resolve => {
mm.addListener(e => {
if(e.matches) {
resolve();
}
});
});
}
static saveData(expects) {
// return early if API does not exist
if(!("connection" in navigator) || navigator.connection.saveData === (expects !== "false")) {
return;
}
// dangly promise
return new Promise(() => {});
}
}
// Should this auto define? Folks can redefine later using { component } export
if("customElements" in window) {
window.customElements.define(Island.tagName, Island);
window.Island = Island;
}
export {
Island,
Island as component, // Backwards compat only: recommend `Island` export
};
// TODO remove in 4.0
export const ready = Island.ready; // Backwards compat only: recommend `Island` export

Binary file not shown.

View file

@ -0,0 +1,117 @@
class Snow extends HTMLElement {
static random(min, max) {
return min + Math.floor(Math.random() * (max - min) + 1);
}
static attrs = {
count: "count", // default: 100
mode: "mode",
}
generateCss(mode, count) {
let css = [];
css.push(`
:host([mode="element"]) {
display: block;
position: relative;
overflow: hidden;
}
:host([mode="page"]) {
position: fixed;
top: 0;
left: 0;
right: 0;
}
:host([mode="page"]),
:host([mode="element"]) > * {
pointer-events: none;
}
:host([mode="element"]) ::slotted(*) {
pointer-events: all;
}
* {
position: absolute;
width: var(--snow-fall-size, 10px);
height: var(--snow-fall-size, 10px);
background: var(--snow-fall-color, rgba(255,255,255,.5));
border-radius: 50%;
}
`);
// using vw units (max 100)
let dimensions = { width: 100, height: 100 };
let units = { x: "vw", y: "vh"};
if(mode === "element") {
dimensions = {
width: this.firstElementChild.clientWidth,
height: this.firstElementChild.clientHeight,
};
units = { x: "px", y: "px"};
}
// Thank you @alphardex: https://codepen.io/alphardex/pen/dyPorwJ
for(let j = 1; j<= count; j++ ) {
let x = Snow.random(1, 100) * dimensions.width/100; // vw
let offset = Snow.random(-10, 10) * dimensions.width/100; // vw
let yoyo = Math.round(Snow.random(30, 100)); // % time
let yStart = yoyo * dimensions.height/100; // vh
let yEnd = dimensions.height; // vh
let scale = Snow.random(1, 10000) * .0001;
let duration = Snow.random(10, 30);
let delay = Snow.random(0, 30) * -1;
css.push(`
:nth-child(${j}) {
opacity: ${Snow.random(0, 1000) * 0.001};
transform: translate(${x}${units.x}, -10px) scale(${scale});
animation: fall-${j} ${duration}s ${delay}s linear infinite;
}
@keyframes fall-${j} {
${yoyo}% {
transform: translate(${x + offset}${units.x}, ${yStart}${units.y}) scale(${scale});
}
to {
transform: translate(${x + offset / 2}${units.x}, ${yEnd}${units.y}) scale(${scale});
}
}`)
}
return css.join("\n");
}
connectedCallback() {
// https://caniuse.com/mdn-api_cssstylesheet_replacesync
if(this.shadowRoot || !("replaceSync" in CSSStyleSheet.prototype)) {
return;
}
let count = parseInt(this.getAttribute(Snow.attrs.count)) || 100;
let mode;
if(this.hasAttribute(Snow.attrs.mode)) {
mode = this.getAttribute(Snow.attrs.mode);
} else {
mode = this.firstElementChild ? "element" : "page";
this.setAttribute(Snow.attrs.mode, mode);
}
let sheet = new CSSStyleSheet();
sheet.replaceSync(this.generateCss(mode, count));
let shadowroot = this.attachShadow({ mode: "open" });
shadowroot.adoptedStyleSheets = [sheet];
let d = document.createElement("div");
for(let j = 0, k = count; j<k; j++) {
shadowroot.appendChild(d.cloneNode());
}
shadowroot.appendChild(document.createElement("slot"));
}
}
customElements.define("snow-fall", Snow);

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
{ {
"/app.js": "/app.js?id=ff1533ec4a7afad65c5bd7bcc2cc7d7b", "/app.js": "/app.js?id=79bae40dcb18de9ca1b5d0008c577471",
"/app-dark.css": "/app-dark.css?id=15c72df05e2b1147fa3e4b0670cfb435", "/app-dark.css": "/app-dark.css?id=15c72df05e2b1147fa3e4b0670cfb435",
"/app.css": "/app.css?id=4d6a1a7fe095eedc2cb2a4ce822ea8a5", "/app.css": "/app.css?id=4d6a1a7fe095eedc2cb2a4ce822ea8a5",
"/img/favicon.png": "/img/favicon.png?id=1542bfe8a0010dcbee710da13cce367f", "/img/favicon.png": "/img/favicon.png?id=1542bfe8a0010dcbee710da13cce367f",

View file

@ -30,6 +30,11 @@
<link rel="pgpkey" href="/assets/jonnybarnes-public-key-ecc.asc"> <link rel="pgpkey" href="/assets/jonnybarnes-public-key-ecc.asc">
</head> </head>
<body class="grid"> <body class="grid">
<is-land on:media="(prefers-reduced-motion: no-preference)">
<snow-fall
style="--snow-fall-color: oklch(36.14% 0.224 277.28)"
></snow-fall>
</is-land>
<header id="site-header"> <header id="site-header">
<h1> <h1>
<a rel="author" href="/">{{ config('user.display_name') }}</a> <a rel="author" href="/">{{ config('user.display_name') }}</a>
@ -73,6 +78,8 @@
<!--scripts go here when needed--> <!--scripts go here when needed-->
@section('scripts') @section('scripts')
<script type="module" src="/assets/frontent/is-land.js"></script>
<script type="module" src="/assets/frontend/snow-fall.js"></script>
<script src="/assets/app.js"></script> <script src="/assets/app.js"></script>
@show @show
</body> </body>

View file

@ -186,6 +186,7 @@ Route::group(['domain' => config('url.longurl')], function () {
// Bookmarks // Bookmarks
Route::group(['prefix' => 'bookmarks'], function () { Route::group(['prefix' => 'bookmarks'], function () {
Route::get('/', [BookmarksController::class, 'index']); Route::get('/', [BookmarksController::class, 'index']);
Route::redirect('/tagged', '/bookmarks');
Route::get('/{bookmark}', [BookmarksController::class, 'show']); Route::get('/{bookmark}', [BookmarksController::class, 'show']);
Route::get('/tagged/{tag}', [BookmarksController::class, 'tagged']); Route::get('/tagged/{tag}', [BookmarksController::class, 'tagged']);
}); });