commit
6ff247d58b
26 changed files with 1471 additions and 858 deletions
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(
|
||||||
[
|
[
|
||||||
|
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
713
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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
1092
package-lock.json
generated
File diff suppressed because it is too large
Load diff
16
package.json
16
package.json
|
@ -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.
338
public/assets/frontend/is-land.js
Normal file
338
public/assets/frontend/is-land.js
Normal 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
|
BIN
public/assets/frontend/is-land.js.br
Normal file
BIN
public/assets/frontend/is-land.js.br
Normal file
Binary file not shown.
117
public/assets/frontend/snow-fall.js
Normal file
117
public/assets/frontend/snow-fall.js
Normal 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);
|
BIN
public/assets/frontend/snow-fall.js.br
Normal file
BIN
public/assets/frontend/snow-fall.js.br
Normal file
Binary file not shown.
2
public/vendor/horizon/app.js
vendored
2
public/vendor/horizon/app.js
vendored
File diff suppressed because one or more lines are too long
2
public/vendor/horizon/mix-manifest.json
vendored
2
public/vendor/horizon/mix-manifest.json
vendored
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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']);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue