Merge pull request #758 from jonnybarnes/develop

MTM 2023 Redesign v1
This commit is contained in:
Jonny Barnes 2023-04-08 17:07:18 +01:00 committed by GitHub
commit 22447b6027
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
74 changed files with 2522 additions and 3450 deletions

View file

@ -1,59 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\Bookmark;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SyndicateBookmarkToTwitter implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* Create a new job instance.
*/
public function __construct(
protected Bookmark $bookmark
) {
}
/**
* Execute the job.
*
* @throws GuzzleException
*/
public function handle(Client $guzzle): void
{
//send webmention
$response = $guzzle->request(
'POST',
'https://brid.gy/publish/webmention',
[
'form_params' => [
'source' => $this->bookmark->longurl,
'target' => 'https://brid.gy/publish/twitter',
'bridgy_omit_link' => 'maybe',
],
]
);
//parse for syndication URL
if ($response->getStatusCode() === 201) {
$json = json_decode((string) $response->getBody());
$syndicates = $this->bookmark->syndicates;
$syndicates['twitter'] = $json->url;
$this->bookmark->syndicates = $syndicates;
$this->bookmark->save();
}
}
}

View file

@ -1,57 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\Note;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SyndicateNoteToTwitter implements ShouldQueue
{
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* Create a new job instance.
*/
public function __construct(
protected Note $note
) {
}
/**
* Execute the job.
*
*
* @throws GuzzleException
*/
public function handle(Client $guzzle)
{
//send webmention
$response = $guzzle->request(
'POST',
'https://brid.gy/publish/webmention',
[
'form_params' => [
'source' => $this->note->longurl,
'target' => 'https://brid.gy/publish/twitter',
'bridgy_omit_link' => 'maybe',
],
]
);
//parse for syndication URL
if ($response->getStatusCode() == 201) {
$json = json_decode((string) $response->getBody());
$tweet_id = basename(parse_url($json->url, PHP_URL_PATH));
$this->note->tweet_id = $tweet_id;
$this->note->save();
}
}
}

View file

@ -279,50 +279,6 @@ class Note extends Model
return $oEmbed; return $oEmbed;
} }
/**
* Show a specific form of the note for twitter.
*
* That is we swap the contacts names for their known Twitter handles.
*/
public function getTwitterContentAttribute(): string
{
$this->getContacts();
// check for contacts
if ($this->contacts === null || count($this->contacts) === 0) {
return '';
}
// here we check the matched contact from the note corresponds to a contact
// in the database
if (
count(array_unique(array_values($this->contacts))) === 1
&& array_unique(array_values($this->contacts))[0] === null
) {
return '';
}
// swap in Twitter usernames
$swapped = preg_replace_callback(
self::USERNAMES_REGEX,
function ($matches) {
if (is_null($this->contacts[$matches[1]])) {
return $matches[0];
}
$contact = $this->contacts[$matches[1]];
if ($contact->twitter) {
return '@' . $contact->twitter;
}
return $contact->name;
},
$this->getRawOriginal('note')
);
return $this->convertMarkdown($swapped);
}
/** /**
* Scope a query to select a note via a NewBase60 id. * Scope a query to select a note via a NewBase60 id.
*/ */

View file

@ -6,9 +6,7 @@ namespace App\Services;
use App\Exceptions\InternetArchiveException; use App\Exceptions\InternetArchiveException;
use App\Jobs\ProcessBookmark; use App\Jobs\ProcessBookmark;
use App\Jobs\SyndicateBookmarkToTwitter;
use App\Models\Bookmark; use App\Models\Bookmark;
use App\Models\SyndicationTarget;
use App\Models\Tag; use App\Models\Tag;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ClientException;
@ -47,23 +45,6 @@ class BookmarkService extends Service
$bookmark->tags()->save($tag); $bookmark->tags()->save($tag);
} }
$mpSyndicateTo = null;
if (Arr::get($request, 'mp-syndicate-to')) {
$mpSyndicateTo = Arr::get($request, 'mp-syndicate-to');
}
if (Arr::get($request, 'properties.mp-syndicate-to')) {
$mpSyndicateTo = Arr::get($request, 'properties.mp-syndicate-to');
}
$mpSyndicateTo = Arr::wrap($mpSyndicateTo);
foreach ($mpSyndicateTo as $uid) {
$target = SyndicationTarget::where('uid', $uid)->first();
if ($target && $target->service_name === 'Twitter') {
SyndicateBookmarkToTwitter::dispatch($bookmark);
break;
}
}
ProcessBookmark::dispatch($bookmark); ProcessBookmark::dispatch($bookmark);
return $bookmark; return $bookmark;

View file

@ -6,7 +6,6 @@ namespace App\Services;
use App\Jobs\SendWebMentions; use App\Jobs\SendWebMentions;
use App\Jobs\SyndicateNoteToMastodon; use App\Jobs\SyndicateNoteToMastodon;
use App\Jobs\SyndicateNoteToTwitter;
use App\Models\Media; use App\Models\Media;
use App\Models\Note; use App\Models\Note;
use App\Models\Place; use App\Models\Place;
@ -50,11 +49,6 @@ class NoteService extends Service
dispatch(new SendWebMentions($note)); dispatch(new SendWebMentions($note));
// Syndication targets
if (in_array('twitter', $this->getSyndicationTargets($request), true)) {
dispatch(new SyndicateNoteToTwitter($note));
}
if (in_array('mastodon', $this->getSyndicationTargets($request), true)) { if (in_array('mastodon', $this->getSyndicationTargets($request), true)) {
dispatch(new SyndicateNoteToMastodon($note)); dispatch(new SyndicateNoteToMastodon($note));
} }

View file

@ -24,7 +24,7 @@
"laravel/horizon": "^5.0", "laravel/horizon": "^5.0",
"laravel/sanctum": "^3.0", "laravel/sanctum": "^3.0",
"laravel/tinker": "^2.0", "laravel/tinker": "^2.0",
"lcobucci/jwt": "^4.0", "lcobucci/jwt": "^5.0",
"league/commonmark": "^2.0", "league/commonmark": "^2.0",
"league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-aws-s3-v3": "^3.0",
"mf2/mf2": "~0.3", "mf2/mf2": "~0.3",

959
composer.lock generated

File diff suppressed because it is too large Load diff

3195
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,42 +4,34 @@
"version": "0.0.1", "version": "0.0.1",
"repository": "https://github.com/jonnybarnes/jonnybarnes.uk", "repository": "https://github.com/jonnybarnes/jonnybarnes.uk",
"license": "CC0-1.0", "license": "CC0-1.0",
"dependencies": {
"normalize.css": "^8.0.1"
},
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.12", "@babel/core": "^7.21.4",
"@babel/preset-env": "^7.20.2", "@babel/preset-env": "^7.21.4",
"autoprefixer": "^10.4.13", "@csstools/postcss-oklab-function": "^2.2.0",
"autoprefixer": "^10.4.14",
"babel-loader": "^9.1.2", "babel-loader": "^9.1.2",
"browserlist": "^1.0.1", "browserlist": "^1.0.1",
"compression-webpack-plugin": "^10.0.0", "compression-webpack-plugin": "^10.0.0",
"css-loader": "^6.7.3", "css-loader": "^6.7.3",
"cssnano": "^5.1.15", "cssnano": "^6.0.0",
"eslint": "^8.34.0", "eslint": "^8.37.0",
"eslint-webpack-plugin": "^4.0.0", "eslint-webpack-plugin": "^4.0.0",
"mini-css-extract-plugin": "^2.7.2", "mini-css-extract-plugin": "^2.7.5",
"postcss": "^8.4.21", "postcss": "^8.4.21",
"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",
"postcss-loader": "^7.0.2", "postcss-loader": "^7.2.4",
"stylelint": "^14.16.1", "postcss-nesting": "^11.2.2",
"stylelint-config-standard": "^29.0.0", "stylelint": "^15.4.0",
"stylelint-webpack-plugin": "^4.0.0", "stylelint-config-standard": "^32.0.0",
"webpack": "^5.75.0", "stylelint-webpack-plugin": "^4.1.0",
"webpack": "^5.78.0",
"webpack-cli": "^5.0.1" "webpack-cli": "^5.0.1"
}, },
"scripts": { "scripts": {
"compress": "scripts/compress", "dev": "webpack",
"copy-dist": "cp ./node_modules/normalize.css/normalize.css ./public/assets/frontend/", "prod": "NODE_ENV=production webpack"
"lint:es6": "eslint resources/es/*.js",
"lint:sass": "stylelint --syntax=scss resources/sass/**/*.scss",
"make-orig": "npm run make:css && npm run make:js",
"make": "npm run lint:sass && npm run webpack",
"make:css": "npm run lint:sass && npm run sass && npm run postcss",
"make:js": "npm run lint:es6 && npm run webpack && npm run uglifyjs",
"webpack": "webpack"
}, },
"browserslist": [ "browserslist": [
"last 2 versions", "last 2 versions",

4
postcss.config.js vendored
View file

@ -2,6 +2,10 @@ module.exports = {
plugins: { plugins: {
'postcss-import': {}, 'postcss-import': {},
'autoprefixer': {}, 'autoprefixer': {},
'@csstools/postcss-oklab-function': {
preserve: true
},
'postcss-nesting': {},
'postcss-combine-media-query': {}, 'postcss-combine-media-query': {},
'postcss-combine-duplicated-selectors': { 'postcss-combine-duplicated-selectors': {
removeDuplicatedProperties: true, removeDuplicatedProperties: true,

View file

@ -1,6 +1 @@
/*!***************************************************************************************************************************************************************************!*\ :root{--font-family-headings:"Archer SSm A","Archer SSm B",serif;--font-family-body:"Verlag A","Verlag B",sans-serif;--font-family-monospace:"Operator Mono SSm A","Operator Mono SSm B",monospace;--font-size-sm:0.75rem;--font-size-base:1rem;--font-size-md:1.25rem;--font-size-lg:1.5rem;--font-size-xl:1.75rem;--font-size-xxl:2rem;--font-size-xxxl:2.25rem;--color-primary:#334800;--color-secondary:#e3ffb5;--color-link:#0064a5;--color-link-visited:#be76ff;--color-primary-shadow:rgba(15,25,0,.4)}@supports (color:color(display-p3 0 0 0)){:root{--color-primary:color(display-p3 0.21567 0.27838 0.03615);--color-secondary:color(display-p3 0.91016 0.99842 0.74082);--color-link:color(display-p3 0.01045 0.38351 0.63618);--color-link-visited:color(display-p3 0.70467 0.47549 0.99958);--color-primary-shadow:color(display-p3 0.06762 0.09646 0.00441/0.4)}}@supports (color:oklch(0% 0 0)){:root{--color-primary:oklch(36.8% 0.1 125.505);--color-secondary:oklch(96.3% 0.1 125.505);--color-link:oklch(48.09% 0.146 241.41);--color-link-visited:oklch(70.44% 0.21 304.41);--color-primary-shadow:oklch(19.56% 0.054 125.505/40%)}}body{background-color:var(--color-secondary);color:var(--color-primary);font-family:var(--font-family-body);font-size:var(--font-size-md)}code{font-family:var(--font-family-monospace)}h1,h2,h3,h4,h5,h6{font-family:var(--font-family-headings)}.grid{display:grid;grid-template-columns:5vw 1fr 5vw;grid-template-rows:-webkit-min-content 1fr -webkit-min-content;grid-template-rows:min-content 1fr min-content;row-gap:1rem}#site-header{grid-column:2/3;grid-row:1/2}main{grid-row:2/3}footer,main{grid-column:2/3}footer{grid-row:3/4}footer .iwc-logo{max-width:85vw}a{color:var(--color-link)}a:visited{color:var(--color-link-visited)}#site-header a:visited{color:var(--color-link)}.hljs{border-radius:.5rem}.p-bridgy-twitter-content{display:none}.h-card .hovercard{-webkit-box-orient:vertical;-webkit-box-direction:normal;background-color:var(--color-secondary);border-radius:1rem;-webkit-box-shadow:0 .5rem .5rem .5rem var(--color-primary-shadow);box-shadow:0 .5rem .5rem .5rem var(--color-primary-shadow);display:none;-ms-flex-direction:column;flex-direction:column;gap:.5rem;opacity:0;padding:1rem;position:absolute;-webkit-transition:opacity .5s ease-in-out;transition:opacity .5s ease-in-out;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;z-index:100}.h-card .hovercard .u-photo{max-width:6rem}.h-card .hovercard .social-icon{height:1rem;width:1rem}.h-card:hover .hovercard{display:-webkit-box;display:-ms-flexbox;display:flex;opacity:1}.h-entry{-webkit-border-start:1px solid var(--color-primary);-webkit-padding-start:.5rem;border-inline-start:1px solid var(--color-primary);padding-inline-start:.5rem}.h-entry .reply-to{font-style:italic}.h-entry .post-info a{text-decoration:none}.h-entry .note-metadata{-webkit-box-orient:horizontal;-webkit-box-direction:normal;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;gap:1rem}.h-entry .note-metadata .syndication-links{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap}.h-entry .note-metadata .syndication-links a{text-decoration:none}.h-entry .note-metadata .syndication-links a svg{height:1rem;width:1rem}
!*** css ./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[0].use[1]!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[0].use[2]!./resources/css/app.css ***!
\***************************************************************************************************************************************************************************/
:root{--font-stack-body:"Whitney SSm A","Whitney SSm B",sans-serif;--font-stack-headings:"Quarto A","Quarto B",serif;--font-stack-monospace:"Operator Mono SSm A","Operator Mono SSm B",monospace;--color-background:#004643;--color-headline:#fffffe;--color-paragraph:#abd1c6;--color-button:#f9bc60;--color-button-text:#001e1d;--color-stroke:#001e1d;--color-main:#e8e4e6;--color-highlight:#f9bc60;--color-secondary:#abd1c6;--color-tertiary:#e16162}body{-webkit-box-orient:vertical;-webkit-box-direction:normal;background-color:var(--color-background);color:var(--color-paragraph);display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;font-family:var(--font-stack-body);font-size:2rem;font-style:normal;font-weight:400}h1,h2,h3,h4,h5,h6{font-family:var(--font-stack-headings);font-style:normal;font-weight:800}code,pre{font-family:var(--font-stack-monospace);font-style:normal;font-weight:400}a{color:var(--color-highlight);text-decoration:none}.h-feed>.h-entry,.h-feed>.note{margin-top:4rem}main{-webkit-box-orient:vertical;-webkit-box-direction:normal;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;margin:auto}main img{max-width:100%}main .h-entry:first-child>.bookmark-link{padding-top:2rem}.note{-webkit-box-orient:vertical;-ms-flex-direction:column;flex-direction:column}.note,.note-metadata{-webkit-box-direction:normal;display:-webkit-box;display:-ms-flexbox;display:flex}.note-metadata{-webkit-box-orient:horizontal;-webkit-box-pack:justify;-ms-flex-pack:justify;-ms-flex-direction:row;flex-direction:row;justify-content:space-between}.note .client{word-break:break-all}.note .syndication-links svg{height:1em;width:1em}.note>.e-content>.naked-link .u-photo{margin:2rem 0}article header>h1{margin-bottom:0}.post-info{font-size:1.4rem}.pagination{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-box-pack:space-evenly;-ms-flex-pack:space-evenly;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;justify-content:space-evenly;list-style-type:none;max-width:90vw}.personal-bio{padding:0 2rem}footer{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;margin-top:1.5rem}.iwc-logo{max-width:100%}#top-header{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-box-pack:center;-ms-flex-pack:center;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;justify-content:center}#top-header h1{text-align:center;width:100%}nav{-webkit-box-pack:center;-ms-flex-pack:center;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;justify-content:center}nav a{margin:0 .5rem}.post-info a,.syndication-links .u-syndication{text-decoration:none}.hovercard,.p-bridgy-facebook-content,.p-bridgy-twitter-content{display:none}@media screen and (max-width:699px){main{margin-left:5px;margin-right:5px}input{max-width:95vw}footer{margin-left:5px;margin-right:5px}}@media screen and (min-width:700px){main{max-width:700px}main>.h-entry,main>.note{padding:0 1rem}}
/*# sourceMappingURL=app.css.map*/

Binary file not shown.

68
public/assets/app.js vendored
View file

@ -1,68 +0,0 @@
/******/ (function() { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ "./resources/css/app.css":
/*!*******************************!*\
!*** ./resources/css/app.css ***!
\*******************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
// extracted by mini-css-extract-plugin
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/make namespace object */
/******/ !function() {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ }();
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
!function() {
/*!*****************************!*\
!*** ./resources/js/app.js ***!
\*****************************/
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _css_app_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../css/app.css */ "./resources/css/app.css");
}();
/******/ })()
;
//# sourceMappingURL=app.js.map

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -2,14 +2,12 @@
This is the code that runs my website, [jonnybarnes.uk](https://jonnybarnes.uk). This is the code that runs my website, [jonnybarnes.uk](https://jonnybarnes.uk).
In theory this is usable by other now :D In theory this is usable by others now 🚀
Set up the database, this software needs [PostgreSQL](https://wwwpostgresql.org) Set up the database, this software needs [PostgreSQL](https://wwwpostgresql.org), after installing:
with the [PostGIS](http://postgis.net) plugin. After installing these:
```shell ```shell
$ createdb -E utf8 db_name $ createdb -E utf8 db_name
$ psql -d db_name -c 'CREATE EXTENSION postgis'
``` ```
First get the code, and make sure youre on the `master` branch. This branch will First get the code, and make sure youre on the `master` branch. This branch will
@ -36,12 +34,11 @@ $ php artisan key:generate
$ php artisan migrate $ php artisan migrate
``` ```
Now we need to edit some config values. In `config/app.php` edit `name`, and in Now we need to edit some config values. In `config/app.php` edit `name`.
`config/syndication.php` edit it to the appropriate values or set targets to `[]`.
Some other things that should be changed. Go to `resources/views/master.blade.php`, Some other things that should be changed. Go to `resources/views/master.blade.php`,
you may not want to link to a projects page. Also in the `<head>` the two last links you may not want to link to a projects page. Also in the `<head>` the two last links
are to my profile pic and pgp key, ammend/remove as desired. are to my profile pic and pgp key, ammend/remove as desired.
Now point your server to `public/index.php` and viola. Essentially this is a Now point your server to `public/index.php` et viola. Essentially this is a
Laravel app so debugging things shouldnt be too hard. Laravel app so debugging things shouldnt be too hard.

10
resources/css/app.css vendored
View file

@ -1,6 +1,6 @@
@import "variables.css"; @import "variables.css";
@import "base.css"; @import "fonts.css";
@import "layout/main.css"; @import "layout.css";
@import "link-style.css"; @import "colours.css";
@import "posse.css"; @import "code.css";
@import "user-profiles.css"; @import "content.css";

View file

@ -1,36 +0,0 @@
body {
font-family: var(--font-stack-body);
font-style: normal;
font-weight: 400;
font-size: 2rem;
background-color: var(--color-background);
color: var(--color-paragraph);
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-stack-headings);
font-style: normal;
font-weight: 800;
}
pre,
code {
font-family: var(--font-stack-monospace);
font-style: normal;
font-weight: 400;
}
a {
color: var(--color-highlight);
text-decoration: none;
}
.h-feed > .note,
.h-feed > .h-entry {
margin-top: 4rem;
}

3
resources/css/code.css vendored Normal file
View file

@ -0,0 +1,3 @@
.hljs {
border-radius: .5rem;
}

18
resources/css/colours.css vendored Normal file
View file

@ -0,0 +1,18 @@
body {
background-color: var(--color-secondary);
color: var(--color-primary);
}
a {
color: var(--color-link);
&:visited {
color: var(--color-link-visited);
}
}
#site-header {
& a:visited {
color: var(--color-link);
}
}

36
resources/css/content.css vendored Normal file
View file

@ -0,0 +1,36 @@
@import "posse.css";
@import "h-card.css";
.h-entry {
border-inline-start: 1px solid var(--color-primary);
padding-inline-start: .5rem;
& .reply-to {
font-style: italic;
}
& .post-info {
& a {
text-decoration: none;
}
}
& .note-metadata {
display: flex;
flex-direction: row;
gap: 1rem;
& .syndication-links {
flex-flow: row wrap;
& a {
text-decoration: none;
& svg {
width: 1rem;
height: 1rem;
}
}
}
}
}

17
resources/css/fonts.css vendored Normal file
View file

@ -0,0 +1,17 @@
body {
font-family: var(--font-family-body);
font-size: var(--font-size-md);
}
code {
font-family: var(--font-family-monospace);
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-family-headings);
}

32
resources/css/h-card.css vendored Normal file
View file

@ -0,0 +1,32 @@
.h-card {
& .hovercard {
display: none;
position: absolute;
z-index: 100;
padding: 1rem;
border-radius: 1rem;
box-shadow: 0 .5rem .5rem .5rem var(--color-primary-shadow);
background-color: var(--color-secondary);
width: fit-content;
transition: opacity 0.5s ease-in-out;
opacity: 0;
flex-direction: column;
gap: .5rem;
& .u-photo {
max-width: 6rem;
}
& .social-icon {
width: 1rem;
height: 1rem;
}
}
&:hover {
& .hovercard {
display: flex;
opacity: 1;
}
}
}

25
resources/css/layout.css vendored Normal file
View file

@ -0,0 +1,25 @@
.grid {
display: grid;
grid-template-columns: 5vw 1fr 5vw;
grid-template-rows: min-content 1fr min-content;
row-gap: 1rem;
}
#site-header {
grid-column: 2 / 3;
grid-row: 1 / 2;
}
main {
grid-column: 2 / 3;
grid-row: 2 / 3;
}
footer {
grid-column: 2 / 3;
grid-row: 3 / 4;
& .iwc-logo {
max-width: 85vw;
}
}

View file

@ -1,125 +0,0 @@
body {
display: flex;
flex-direction: column;
}
@media screen and (max-width: 699px) {
main {
margin-left: 5px;
margin-right: 5px;
}
}
@media screen and (min-width: 700px) {
main {
max-width: 700px;
}
main > .note,
main > .h-entry {
padding: 0 1rem;
}
}
main {
display: flex;
flex-direction: column;
margin: auto;
}
main img {
max-width: 100%;
}
main .h-entry:first-child > .bookmark-link {
padding-top: 2rem;
}
.note {
display: flex;
flex-direction: column;
}
.note-metadata {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.note .client {
word-break: break-all;
}
.note .syndication-links svg {
height: 1em;
width: 1em;
}
.note > .e-content > .naked-link .u-photo {
margin: 2rem 0;
}
article header > h1 {
margin-bottom: 0;
}
.post-info {
font-size: 1.4rem;
}
.pagination {
display: flex;
flex-flow: row wrap;
justify-content: space-evenly;
max-width: 90vw;
list-style-type: none;
}
.personal-bio {
padding: 0 2rem;
}
footer {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 1.5rem;
}
@media screen and (max-width: 699px) {
input {
max-width: 95vw;
}
}
.iwc-logo {
max-width: 100%;
}
@media screen and (max-width: 699px) {
footer {
margin-left: 5px;
margin-right: 5px;
}
}
#top-header {
display: flex;
flex-direction: column;
justify-content: center;
}
#top-header h1 {
width: 100%;
text-align: center;
}
nav {
display: flex;
justify-content: center;
flex-wrap: wrap;
}
nav a {
margin: 0 0.5rem;
}

View file

@ -1,7 +0,0 @@
.post-info a {
text-decoration: none;
}
.syndication-links .u-syndication {
text-decoration: none;
}

View file

@ -1,4 +1,3 @@
.p-bridgy-facebook-content,
.p-bridgy-twitter-content { .p-bridgy-twitter-content {
display: none; display: none;
} }

View file

@ -1,3 +0,0 @@
.hovercard {
display: none;
}

View file

@ -1,20 +1,22 @@
:root { :root {
/* fonts */ /* Font Family */
--font-stack-body: "Whitney SSm A", "Whitney SSm B", sans-serif; --font-family-headings: "Archer SSm A", "Archer SSm B", serif;
--font-stack-headings: "Quarto A", "Quarto B", serif; --font-family-body: "Verlag A", "Verlag B", sans-serif;
--font-stack-monospace: "Operator Mono SSm A", "Operator Mono SSm B", monospace; --font-family-monospace: "Operator Mono SSm A", "Operator Mono SSm B", monospace;
/* colours */ /* Font Size */
--color-background: #004643; --font-size-sm: 0.75rem; /* 12px */
--color-headline: #fffffe; --font-size-base: 1rem; /* 16px, base */
--color-paragraph: #abd1c6; --font-size-md: 1.25rem; /* 20px */
--color-button: #f9bc60; --font-size-lg: 1.5rem; /* 24px */
--color-button-text: #001e1d; --font-size-xl: 1.75rem; /* 28px */
--font-size-xxl: 2rem; /* 32px */
--font-size-xxxl: 2.25rem; /* 36px */
/* colours - illustrations */ /* Colours */
--color-stroke: #001e1d; --color-primary: oklch(36.8% 0.1 125.505);
--color-main: #e8e4e6; --color-secondary: oklch(96.3% 0.1 125.505);
--color-highlight: #f9bc60; --color-link: oklch(48.09% 0.146 241.41);
--color-secondary: #abd1c6; --color-link-visited: oklch(70.44% 0.21 304.41);
--color-tertiary: #e16162; --color-primary-shadow: oklch(19.56% 0.054 125.505 / 40%);
} }

View file

@ -1,29 +0,0 @@
//colours.js
let link = document.querySelector('#colourScheme');
let css = link.getAttribute('href').split('/').pop();
// update selected item in colour scheme list
document.querySelector('#colourSchemeSelect [value="' + css + '"]').selected = true;
// fix form
let form = document.getElementById('colourSchemeForm');
let btn = form.querySelector('button');
btn.addEventListener('click', function (event) {
event.preventDefault();
let newCss = document.getElementById('colourSchemeSelect').value;
let css = link.getAttribute('href');
let parts = css.split('/');
parts.pop();
parts.push(newCss);
link.setAttribute('href', parts.join('/'));
let formData = new FormData(form);
fetch('/update-colour-scheme', {
method: 'POST',
credentials: 'same-origin',
body: formData
}).catch(function (error) {
console.warn(error);
});
});

View file

@ -1,7 +0,0 @@
//edit-place-icon.js
export default function getIcon() {
let iconOption = document.querySelector('#icon');
return iconOption.value;
}

View file

@ -1,31 +0,0 @@
//links.js
let youtubeRegex = /watch\?v=([A-Za-z0-9\-_]+)\b/;
let spotifyRegex = /https:\/\/play\.spotify\.com\/(.*)\b/;
let notes = document.querySelectorAll('.e-content');
for (let note of notes) {
let ytid = note.textContent.match(youtubeRegex);
if (ytid) {
let ytcontainer = document.createElement('div');
ytcontainer.classList.add('container');
let ytiframe = document.createElement('iframe');
ytiframe.classList.add('youtube');
ytiframe.setAttribute('src', 'https://www.youtube.com/embed/' + ytid[1]);
ytiframe.setAttribute('frameborder', 0);
ytiframe.setAttribute('allowfullscreen', 'true');
ytcontainer.appendChild(ytiframe);
note.appendChild(ytcontainer);
}
let spotifyid = note.textContent.match(spotifyRegex);
if (spotifyid) {
let sid = spotifyid[1].replace('/', ':');
let siframe = document.createElement('iframe');
siframe.classList.add('spotify');
siframe.setAttribute('src', 'https://embed.spotify.com/?uri=spotify:' + sid);
siframe.setAttribute('frameborder', 0);
siframe.setAttribute('allowtransparency', 'true');
note.appendChild(siframe);
}
}

View file

@ -1,192 +0,0 @@
//mapbox-utils.js
import parseLocation from './parse-location';
import selectPlaceInForm from './select-place';
/*
* For specific lines on this file I have disabled the `no-undef`
* rule in ESLint. This is for the mapboxgl object which is defined
* in the mapbox-gl.js file that we pull in from their CDN before
* this script gets run.
*/
// eslint-disable-next-line no-undef
mapboxgl.accessToken = 'pk.eyJ1Ijoiam9ubnliYXJuZXMiLCJhIjoiY2l2cDhjYW04MDAwcjJ0cG1uZnhqcm82ayJ9.qA2zeVA-nsoMh9IFrd5KQw';
// Define some functions to be used in the default function.
const titlecase = (string) => {
return string.split('-').map(([first,...rest]) => first.toUpperCase() + rest.join('').toLowerCase()).join(' ');
};
// Get the ID for the map, i.e. get the u-url of the containing note.
const getId = (map) => {
let href = map._container.parentNode.querySelector('.u-url').getAttribute('href');
return href.substr(href.lastIndexOf('/') + 1);
};
const addMapTypeOption = (map, menu, option, checked = false) => {
let div = document.createElement('div');
let input = document.createElement('input');
let id = option + getId(map);
input.setAttribute('id', id);
input.setAttribute('type', 'radio');
input.setAttribute('name', 'map' + getId(map));
input.setAttribute('value', option);
if (checked == true) {
input.setAttribute('checked', 'checked');
}
input.addEventListener('click', function () {
let source = map.getSource('points');
map.setStyle('mapbox://styles/mapbox/' + option + '-v9');
map.on('style.load', function () {
map.addLayer({
'id': 'points',
'type': 'symbol',
'source': {
'type': 'geojson',
'data': source._data
},
'layout': {
'icon-image': '{icon}-15',
'text-field': '{title}',
'text-offset': [0, 1]
}
});
});
});
let label = document.createElement('label');
label.setAttribute('for', option + getId(map));
label.appendChild(document.createTextNode(titlecase(option)));
div.appendChild(input);
div.appendChild(label);
menu.appendChild(div);
};
const makeMapMenu = (map) => {
let mapMenu = document.createElement('fieldset');
let legend = document.createElement('legend');
let title = document.createTextNode('Map Style');
legend.appendChild(title);
mapMenu.appendChild(legend);
mapMenu.classList.add('map-menu');
addMapTypeOption(map, mapMenu, 'streets', true);
addMapTypeOption(map, mapMenu, 'satellite-streets');
return mapMenu;
};
// The main function.
export default function addMap(div, position = null, places = null) {
let data;
let dataLatitude = div.dataset.latitude;
let dataLongitude = div.dataset.longitude;
let dataName = div.dataset.name;
let dataMarker = div.dataset.marker;
if (dataMarker == '') {
dataMarker = 'circle';
}
if (dataName == null) {
data = {
'type': 'FeatureCollection',
'features': [{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [dataLongitude, dataLatitude]
},
'properties': {
'title': 'Current Location',
'icon': 'circle-stroked',
'uri': 'current-location'
}
}]
};
} else {
data = {
'type': 'FeatureCollection',
'features': [{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [dataLongitude, dataLatitude]
},
'properties': {
'title': dataName,
'icon': dataMarker,
}
}]
};
}
if (places != null) {
for (let place of places) {
let placeLongitude = parseLocation(place.location).longitude;
let placeLatitude = parseLocation(place.location).latitude;
data.features.push({
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [placeLongitude, placeLatitude]
},
'properties': {
'title': place.name,
'icon': 'circle',
'uri': place.slug
}
});
}
}
if (position != null) {
dataLongitude = position.coords.longitude;
dataLatitude = position.coords.latitude;
}
// eslint-disable-next-line no-undef
let map = new mapboxgl.Map({
container: div,
style: 'mapbox://styles/mapbox/streets-v9',
center: [dataLongitude, dataLatitude],
zoom: 15
});
if (position == null) {
map.scrollZoom.disable();
}
// eslint-disable-next-line no-undef
map.addControl(new mapboxgl.NavigationControl());
div.appendChild(makeMapMenu(map));
map.on('load', function () {
map.addLayer({
'id': 'points',
'type': 'symbol',
'source': {
'type': 'geojson',
'data': data
},
'layout': {
'icon-image': '{icon}-15',
'text-field': '{title}',
'text-offset': [0, 1]
}
});
});
if (position != null) {
map.on('click', function (e) {
let features = map.queryRenderedFeatures(e.point, {
layer: ['points']
});
// if there are features within the given radius of the click event,
// fly to the location of the click event
if (features.length) {
// Get coordinates from the symbol and center the map on those coordinates
map.flyTo({center: features[0].geometry.coordinates});
selectPlaceInForm(features[0].properties.uri);
}
});
}
if (data.features && data.features.length > 1) {
// eslint-disable-next-line no-undef
let bounds = new mapboxgl.LngLatBounds();
for (let feature of data.features) {
bounds.extend(feature.geometry.coordinates);
}
map.fitBounds(bounds, { padding: 65});
}
return map;
}

View file

@ -1,8 +0,0 @@
//maps.js
import addMap from './mapbox-utils';
let mapDivs = document.querySelectorAll('.map');
for (let div of mapDivs) {
addMap(div);
}

View file

@ -1,10 +0,0 @@
//parse-location.js
//text = `POINT(lon lat)`
export default function parseLocation(text) {
let coords = /POINT\((.*)\)/.exec(text);
let parsedLongitude = coords[1].split(' ')[0];
let parsedLatitude = coords[1].split(' ')[1];
return {'latitude': parsedLatitude, 'longitude': parsedLongitude};
}

View file

@ -1,12 +0,0 @@
/* global process */
// Piwik in its own js file to allow usage with a CSP policy
var idSite = process.env.PIWIK_ID;
var piwikTrackingApiUrl = process.env.PIWIK_URL;
var _paq = _paq || [];
// tracker methods like "setCustomDimension" should be called before "trackPageView"
_paq.push(['setTrackerUrl', piwikTrackingApiUrl]);
_paq.push(['setSiteId', idSite]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);

View file

@ -1,83 +0,0 @@
//places.js
import addMap from './mapbox-utils';
import getIcon from './edit-place-icon';
let div = document.querySelector('.map');
let map = addMap(div);
let isDragging;
let isCursorOverPoint;
let canvas = map.getCanvasContainer();
let selectElem = document.querySelector('select[name="icon"]');
selectElem.addEventListener('click', function () {
let newIcon = getIcon();
let source = map.getSource('points');
if (source._data.features[0].properties.icon != newIcon) {
source._data.features[0].properties.icon = newIcon;
map.getSource('points').setData(source._data);
}
});
function updateFormCoords(coords) {
let latInput = document.querySelector('#latitude');
let lonInput = document.querySelector('#longitude');
latInput.value = coords.lat.toPrecision(6);
lonInput.value = coords.lng.toPrecision(6);
}
function mouseDown() {
if (!isCursorOverPoint) return;
isDragging = true;
// Set a cursor indicator
canvas.style.cursor = 'grab';
// Mouse events
map.on('mousemove', onMove);
map.once('mouseup', onUp);
}
function onMove(e) {
if (!isDragging) return;
let coords = e.lngLat;
let source = map.getSource('points');
// Set a UI indicator for dragging.
canvas.style.cursor = 'grabbing';
// Update the Point feature in `geojson` coordinates
// and call setData to the source layer `point` on it.
source._data.features[0].geometry.coordinates = [coords.lng, coords.lat];
map.getSource('points').setData(source._data);
}
function onUp(e) {
if (!isDragging) return;
let coords = e.lngLat;
// Print the coordinates of where the point had
// finished being dragged to on the map.
updateFormCoords(coords);
canvas.style.cursor = '';
isDragging = false;
// Unbind mouse events
map.off('mousemove', onMove);
}
// When the cursor enters a feature in the point layer, prepare for dragging.
map.on('mouseenter', 'points', function() {
canvas.style.cursor = 'move';
isCursorOverPoint = true;
map.dragPan.disable();
});
map.on('mouseleave', 'points', function() {
canvas.style.cursor = '';
isCursorOverPoint = false;
map.dragPan.enable();
});
map.on('mousedown', mouseDown);

View file

@ -1,11 +0,0 @@
//select-place.js
export default function selectPlaceInForm(uri) {
if (document.querySelector('select')) {
if (uri == 'current-location') {
document.querySelector('select [id="option-coords"]').selected = true;
} else {
document.querySelector('select [value="' + uri + '"]').selected = true;
}
}
}

View file

@ -1,68 +0,0 @@
//submit-place.js
export default function submitNewPlace(map) {
//create the form data to send
let formData = new FormData();
formData.append('place-name', document.querySelector('#place-name').value);
formData.append('place-description', document.querySelector('#place-description').value);
formData.append('place-latitude', document.querySelector('#place-latitude').value);
formData.append('place-longitude', document.querySelector('#place-longitude').value);
//post the new place
fetch('/micropub/places', {
//send cookies with the request
credentials: 'same-origin',
method: 'post',
body: formData
}).then(function (response) {
return response.json();
}).then(function (placeJson) {
if (placeJson.error === true) {
throw new Error(placeJson.error_description);
}
//remove un-needed form elements
let form = document.querySelector('fieldset');
//iterate through labels and remove parent div elements
let labels = document.querySelectorAll('.place-label');
for (let label of labels) {
form.removeChild(label.parentNode);
}
form.removeChild(document.querySelector('#place-submit'));
let newPlaceButton = document.querySelector('#create-new-place');
//in order to remove a DOM Node, you need to run removeChild on the parent Node
newPlaceButton.parentNode.removeChild(newPlaceButton);
//remove current location from map
let source = map.getSource('points');
let newFeatures = source._data.features.filter(function (item) {
return item.properties.title != 'Current Location';
});
//add new place to map
newFeatures.push({
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [placeJson.longitude, placeJson.latitude]
},
'properties': {
'title': placeJson.name,
'icon': 'circle',
'uri': placeJson.uri
}
});
let newSource = {
'type': 'FeatureCollection',
'features': newFeatures
};
map.getSource('points').setData(newSource);
//add new place to select menu
let selectElement = document.querySelector('select');
let newlyCreatedPlaceOption = document.createElement('option');
newlyCreatedPlaceOption.setAttribute('value', placeJson.uri);
newlyCreatedPlaceOption.appendChild(document.createTextNode(placeJson.name));
newlyCreatedPlaceOption.dataset.latitude = placeJson.latitude;
newlyCreatedPlaceOption.dataset.longitude = placeJson.longitude;
selectElement.appendChild(newlyCreatedPlaceOption);
document.querySelector('select [value="' + placeJson.uri + '"]').selected = true;
}).catch(function (placeError) {
console.error(placeError);
});
}

View file

@ -1,5 +0,0 @@
//articles.scss
article.h-entry {
width: $mainWidth;
}

View file

@ -1,22 +0,0 @@
// _base.scss
// Fonts
html {
font-family: montserrat, sans-serif;
font-weight: 300;
font-style: normal;
font-size: 20px;
}
h1 {
font-family: bebas-neue, sans-serif;
font-weight: 400;
font-style: normal;
}
code {
font-family: "Operator Mono", monospace;
}
// Variables
$mainWidth: 40rem;

View file

@ -1,11 +0,0 @@
// bio.scss
.personal-bio {
width: $mainWidth;
}
@media screen and (max-width: $mainWidth) {
.personal-bio {
width: 95vw;
}
}

View file

@ -1,49 +0,0 @@
//emoji.scss
//thanks to http://adrianroselli.com/2016/12/accessible-emoji-tweaked.html
span[role=img][aria-label] {
position: relative;
}
span[role=img][aria-label]:focus::after,
span[role=img][aria-label]:hover::after {
position: absolute;
display: block;
z-index: 1;
bottom: 1.5em;
left: 0;
padding: 0.5em 0.75em;
border: 0.05em solid rgba(255, 255, 255, 1);
border-radius: 0.2em;
box-shadow: 0.15em 0.15em 0.5em rgba(0, 0, 0, 1);
content: attr(aria-label);
background-color: rgba(0, 0, 0, 0.85);
color: rgba(255, 255, 255, 1);
font-size: 80%;
animation: TOOLTIP 0.1s ease-out 1;
}
@keyframes TOOLTIP {
from {
bottom: 0.5em;
background-color: rgba(0, 0, 0, 0);
border: 0.05em solid rgba(255, 255, 255, 0);
color: rgba(255, 255, 255, 0);
box-shadow: 0 0 0 rgba(0, 0, 0, 1);
}
to {
bottom: 1.5em;
background-color: rgba(0, 0, 0, 0.85);
border: 0.05em solid rgba(255, 255, 255, 1);
color: rgba(255, 255, 255, 1);
box-shadow: 0.15em 0.15em 0.5em rgba(0, 0, 0, 1);
}
}
@media print {
span[role=img][aria-label]::after {
content: " (" attr(aria-label) ") ";
}
}

View file

@ -1,38 +0,0 @@
// footer.scss
footer {
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--white);
color: var(--black);
a {
color: var(--blue);
text-decoration: none;
}
form:first-child {
margin-bottom: 1rem;
}
p {
margin-bottom: 0;
}
}
@media screen and (max-width: $mainWidth) {
footer {
img {
width: 95%;
}
form {
width: 95vw;
}
select {
width: 90vw;
}
}
}

View file

@ -1,21 +0,0 @@
// main.scss
main {
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--white);
color: var(--black);
a {
color: var(--blue);
text-decoration: none;
}
.h-entry {
margin: 3rem 0;
}
}
@import "pagination";
@import "bio";

View file

@ -1,5 +0,0 @@
//mapbox.scss
.map {
height: 200px;
}

View file

@ -1,17 +0,0 @@
// notes.scss
.h-entry .note {
width: $mainWidth;
}
@media screen and (max-width: $mainWidth) {
.h-entry .note {
width: 95vw;
.e-content {
img {
width: 100%;
}
}
}
}

View file

@ -1,16 +0,0 @@
// pagination.scss
.pagination {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
list-style-type: none;
width: $mainWidth;
}
@media screen and (max-width: $mainWidth) {
.pagination {
width: 95vw;
}
}

View file

@ -1,25 +0,0 @@
// site-header.scss
#topheader {
display: flex;
width: 100vw;
flex-direction: column;
align-items: center;
background-color: var(--black);
a {
color: var(--white);
text-decoration: none;
}
h1 {
margin-top: 0;
padding-top: 1rem;
font-size: 3rem;
}
nav {
padding-bottom: 1rem;
font-size: 1.5rem;
}
}

View file

@ -1,16 +0,0 @@
//syndication.scss
.note-metadata {
display: flex;
flex-direction: row;
justify-content: space-between;
.social-links {
flex-direction: row;
svg {
height: 1em;
width: auto;
}
}
}

View file

@ -1,18 +0,0 @@
// app.scss
// General styles
@import "base";
@import "site-header";
@import "main";
@import "articles";
@import "notes";
@import "footer";
// Mapbox styles
@import "mapbox";
// The syndication links at the end of notes
@import "syndication";
// Accessible emoji
@import "emoji";

View file

@ -1,36 +0,0 @@
body {
font-family: var(--font-stack-body);
font-style: normal;
font-weight: 400;
font-size: 2rem;
background-color: var(--color-background);
color: var(--color-paragraph);
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-stack-headings);
font-style: normal;
font-weight: 800;
}
pre,
code {
font-family: var(--font-stack-monospace);
font-style: normal;
font-weight: 400;
}
a {
color: var(--color-highlight);
text-decoration: none;
}
.h-feed > .note,
.h-feed > .h-entry {
margin-top: 4rem;
}

View file

@ -1,132 +0,0 @@
body {
display: flex;
flex-direction: column;
}
main {
@media screen and (max-width: 699px) {
margin-left: 5px;
margin-right: 5px;
}
display: flex;
flex-direction: column;
margin: auto;
img {
max-width: 100%;
}
@media screen and (min-width: 700px) {
max-width: 700px;
> .note,
> .h-entry {
padding: 0 1rem;
}
}
.h-entry:first-child {
> .bookmark-link {
padding-top: 2rem;
}
}
}
.note {
display: flex;
flex-direction: column;
.note-metadata {
display: flex;
flex-direction: row;
justify-content: space-between;
.client {
word-break: break-all;
}
.syndication-links {
svg {
height: 1em;
width: 1em;
}
}
}
> .e-content {
> .naked-link {
.u-photo {
margin: 2rem 0;
}
}
}
}
article {
header {
> h1 {
margin-bottom: 0;
}
.post-info {
font-size: 1.4rem;
}
}
}
.pagination {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
max-width: 90vw;
list-style-type: none;
}
.personal-bio {
padding: 0 2rem;
}
footer {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 1.5rem;
input {
@media screen and (max-width: 699px) {
max-width: 95vw;
}
}
.iwc-logo {
max-width: 100%;
}
@media screen and (max-width: 699px) {
margin-left: 5px;
margin-right: 5px;
}
}
#top-header {
display: flex;
flex-direction: column;
justify-content: center;
h1 {
width: 100%;
text-align: center;
}
nav {
display: flex;
justify-content: center;
flex-wrap: wrap;
a {
margin: 0 0.5rem;
}
}
}

View file

@ -1,11 +0,0 @@
.post-info {
a {
text-decoration: none;
}
}
.syndication-links {
.u-syndication {
text-decoration: none;
}
}

View file

@ -1,4 +0,0 @@
.p-bridgy-facebook-content,
.p-bridgy-twitter-content {
display: none;
}

View file

@ -1,20 +0,0 @@
:root {
/* fonts */
--font-stack-body: "Whitney SSm A", "Whitney SSm B", sans-serif;
--font-stack-headings: "Quarto A", "Quarto B", serif;
--font-stack-monospace: "Operator Mono SSm A", "Operator Mono SSm B", monospace;
/* colours */
--color-background: #004643;
--color-headline: #fffffe;
--color-paragraph: #abd1c6;
--color-button: #f9bc60;
--color-button-text: #001e1d;
/* colours - illustrations */
--color-stroke: #001e1d;
--color-main: #e8e4e6;
--color-highlight: #f9bc60;
--color-secondary: #abd1c6;
--color-tertiary: #e16162;
}

View file

@ -1,5 +0,0 @@
@import "variables";
@import "base";
@import "layout-main";
@import "link-styles";
@import "posse";

View file

@ -4,7 +4,4 @@
@section('content') @section('content')
@include('templates.like', ['like' => $like]) @include('templates.like', ['like' => $like])
<!-- POSSE to Twitter -->
<a href="https://brid.gy/publish/twitter"></a>
@stop @stop

View file

@ -4,8 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>@yield('title'){{ config('app.display_name') }}</title> <title>@yield('title'){{ config('app.display_name') }}</title>
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="/assets/frontend/normalize.css"> @if (!empty(config('app.font_link')))
@if (!empty(config('app.font_link')))<link rel="stylesheet" href="{{ config('app.font_link') }}">@endif <link rel="stylesheet" href="{{ config('app.font_link') }}">
@endif
<link rel="stylesheet" href="/assets/app.css"> <link rel="stylesheet" href="/assets/app.css">
<link rel="stylesheet" href="/assets/highlight/zenburn.css"> <link rel="stylesheet" href="/assets/highlight/zenburn.css">
<link rel="alternate" type="application/rss+xml" title="Blog RSS Feed" href="/blog/feed.rss"> <link rel="alternate" type="application/rss+xml" title="Blog RSS Feed" href="/blog/feed.rss">
@ -22,11 +23,11 @@
<link rel="token_endpoint" href="{{ config('app.url') }}/api/token"> <link rel="token_endpoint" href="{{ config('app.url') }}/api/token">
<link rel="micropub" href="{{ config('app.url') }}/api/post"> <link rel="micropub" href="{{ config('app.url') }}/api/post">
<link rel="webmention" href="{{ config('app.url') }}/webmention"> <link rel="webmention" href="{{ config('app.url') }}/webmention">
<link rel="shortcut icon" href="/assets/img/jmb-bw.png"> <link rel="shortcut icon" href="{{ config('app.url') }}/assets/img/memoji-orange-bg-small-fs8.png">
<link rel="pgpkey" href="/assets/jonnybarnes-public-key-ecc.asc"> <link rel="pgpkey" href="/assets/jonnybarnes-public-key-ecc.asc">
</head> </head>
<body> <body class="grid">
<header id="top-header"> <header id="site-header">
<h1> <h1>
<a rel="author" href="/">{{ config('app.display_name') }}</a> <a rel="author" href="/">{{ config('app.display_name') }}</a>
</h1> </h1>
@ -58,22 +59,5 @@
<!--scripts go here when needed--> <!--scripts go here when needed-->
@section('scripts') @section('scripts')
@show @show
@if(config('fathom.id'))
<!-- Fathom - simple website analytics - https://github.com/usefathom/fathom -->
<script>
(function(f, a, t, h, o, m){
a[h]=a[h]||function(){
(a[h].q=a[h].q||[]).push(arguments)
};
o=f.createElement('script'),
m=f.getElementsByTagName('script')[0];
o.async=1; o.src=t; o.id='fathom-script';
m.parentNode.insertBefore(o,m)
})(document, window, '//fathom.jonnybarnes.uk/tracker.js', 'fathom');
fathom('set', 'siteId', '{{ config('fathom.id') }}');
fathom('trackPageview');
</script>
<!-- / Fathom -->
@endif
</body> </body>
</html> </html>

View file

@ -46,8 +46,6 @@
</p> </p>
@endforeach @endforeach
@endif @endif
<!-- this empty tags are for https://brid.gys publishing service -->
<a href="https://brid.gy/publish/twitter"></a>
@stop @stop
@section('scripts') @section('scripts')

View file

@ -26,7 +26,4 @@
@endforeach @endforeach
</ul> </ul>
@endif @endif
<p class="p-bridgy-twitter-content">🔖 {{ $bookmark->url }} 🔗 {{ $bookmark->longurl }}</p>
<!-- these empty tags are for https://brid.gys publishing service -->
<a href="https://brid.gy/publish/twitter"></a>
</div> </div>

View file

@ -1 +1,16 @@
<span class="u-category h-card mini-h-card"><a class="u-url p-name" href="{{ $contact->homepage }}">{!! $contact->name !!}</a><span class="hovercard">@if ($contact->facebook)<a class="u-url" href="https://www.facebook.com/{{ $contact->facebook }}"><img class="social-icon" src="/assets/img/social-icons/facebook.svg"> Facebook</a>@endif @if ($contact->twitter)<a class="u-url" href="https://twitter.com/{{ $contact->twitter }}"><img class="social-icon" src="/assets/img/social-icons/twitter.svg"> {{ $contact->twitter }}</a>@endif<img class="u-photo" alt="" src="{{ $contact->photo }}"></span></span> <span class="u-category h-card mini-h-card">
<a class="u-url p-name" href="{{ $contact->homepage }}">{!! $contact->name !!}</a>
<span class="hovercard">
<img class="u-photo" alt="" src="{{ $contact->photo }}">
@if ($contact->facebook)
<a class="u-url" href="https://www.facebook.com/{{ $contact->facebook }}">
<img class="social-icon" src="/assets/img/social-icons/facebook.svg"> Facebook
</a>
@endif
@if ($contact->twitter)
<a class="u-url" href="https://twitter.com/{{ $contact->twitter }}">
<img class="social-icon" src="/assets/img/social-icons/twitter.svg"> {{ $contact->twitter }}
</a>
@endif
</span>
</span>

View file

@ -26,11 +26,6 @@
@endif @endif
@endforeach @endforeach
</div> </div>
@if ($note->twitter_content)
<div class="p-bridgy-twitter-content">
{!! $note->twitter_content !!}
</div>
@endif
<div class="note-metadata"> <div class="note-metadata">
<div> <div>
<a class="u-url" href="/notes/{{ $note->nb60id }}"> <a class="u-url" href="/notes/{{ $note->nb60id }}">

View file

@ -5,9 +5,7 @@ declare(strict_types=1);
namespace Tests\Feature; namespace Tests\Feature;
use App\Jobs\ProcessBookmark; use App\Jobs\ProcessBookmark;
use App\Jobs\SyndicateBookmarkToTwitter;
use App\Models\Bookmark; use App\Models\Bookmark;
use App\Models\SyndicationTarget;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Queue;
use Tests\TestCase; use Tests\TestCase;
@ -37,25 +35,16 @@ class BookmarksTest extends TestCase
{ {
Queue::fake(); Queue::fake();
SyndicationTarget::factory()->create([
'uid' => 'https://twitter.com/jonnybarnes',
'service_name' => 'Twitter',
]);
$response = $this->withHeaders([ $response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->getToken(), 'Authorization' => 'Bearer ' . $this->getToken(),
])->post('/api/post', [ ])->post('/api/post', [
'h' => 'entry', 'h' => 'entry',
'bookmark-of' => 'https://example.org/blog-post', 'bookmark-of' => 'https://example.org/blog-post',
'mp-syndicate-to' => [
'https://twitter.com/jonnybarnes',
],
]); ]);
$response->assertJson(['response' => 'created']); $response->assertJson(['response' => 'created']);
Queue::assertPushed(ProcessBookmark::class); Queue::assertPushed(ProcessBookmark::class);
Queue::assertPushed(SyndicateBookmarkToTwitter::class);
$this->assertDatabaseHas('bookmarks', ['url' => 'https://example.org/blog-post']); $this->assertDatabaseHas('bookmarks', ['url' => 'https://example.org/blog-post']);
} }
@ -64,52 +53,18 @@ class BookmarksTest extends TestCase
{ {
Queue::fake(); Queue::fake();
SyndicationTarget::factory()->create([
'uid' => 'https://twitter.com/jonnybarnes',
'service_name' => 'Twitter',
]);
$response = $this->withHeaders([ $response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->getToken(), 'Authorization' => 'Bearer ' . $this->getToken(),
])->json('POST', '/api/post', [ ])->json('POST', '/api/post', [
'type' => ['h-entry'], 'type' => ['h-entry'],
'properties' => [ 'properties' => [
'bookmark-of' => ['https://example.org/blog-post'], 'bookmark-of' => ['https://example.org/blog-post'],
'mp-syndicate-to' => [
'https://twitter.com/jonnybarnes',
],
], ],
]); ]);
$response->assertJson(['response' => 'created']); $response->assertJson(['response' => 'created']);
Queue::assertPushed(ProcessBookmark::class); Queue::assertPushed(ProcessBookmark::class);
Queue::assertPushed(SyndicateBookmarkToTwitter::class);
$this->assertDatabaseHas('bookmarks', ['url' => 'https://example.org/blog-post']);
}
/** @test */
public function whenTheBookmarkIsMarkedForPostingToTwitterCheckWeInvokeTheCorrectJob(): void
{
Queue::fake();
SyndicationTarget::factory()->create([
'uid' => 'https://twitter.com/jonnybarnes',
'service_name' => 'Twitter',
]);
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->getToken(),
])->post('/api/post', [
'h' => 'entry',
'bookmark-of' => 'https://example.org/blog-post',
'mp-syndicate-to' => 'https://twitter.com/jonnybarnes',
]);
$response->assertJson(['response' => 'created']);
Queue::assertPushed(ProcessBookmark::class);
Queue::assertPushed(SyndicateBookmarkToTwitter::class);
$this->assertDatabaseHas('bookmarks', ['url' => 'https://example.org/blog-post']); $this->assertDatabaseHas('bookmarks', ['url' => 'https://example.org/blog-post']);
} }

View file

@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace Tests\Feature;
use App\Models\Contact;
use App\Models\Note;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class BridgyPosseTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function notesWeWantCopiedToTwitterShouldHaveNecessaryMarkup(): void
{
Contact::factory()->create([
'nick' => 'joe',
'twitter' => 'joe__',
]);
$note = Note::factory()->create(['note' => 'Hi @joe']);
$response = $this->get($note->longurl);
$html = $response->content();
$this->assertStringContainsString('Hi @joe__', $html);
}
}

View file

@ -6,7 +6,6 @@ namespace Tests\Feature;
use App\Jobs\SendWebMentions; use App\Jobs\SendWebMentions;
use App\Jobs\SyndicateNoteToMastodon; use App\Jobs\SyndicateNoteToMastodon;
use App\Jobs\SyndicateNoteToTwitter;
use App\Models\Media; use App\Models\Media;
use App\Models\Note; use App\Models\Note;
use App\Models\Place; use App\Models\Place;
@ -145,7 +144,6 @@ class MicropubControllerTest extends TestCase
'h' => 'entry', 'h' => 'entry',
'content' => $note, 'content' => $note,
'mp-syndicate-to' => [ 'mp-syndicate-to' => [
'https://twitter.com/jonnybarnes',
'https://mastodon.social/@jonnybarnes', 'https://mastodon.social/@jonnybarnes',
], ],
], ],
@ -153,7 +151,6 @@ class MicropubControllerTest extends TestCase
); );
$response->assertJson(['response' => 'created']); $response->assertJson(['response' => 'created']);
$this->assertDatabaseHas('notes', ['note' => $note]); $this->assertDatabaseHas('notes', ['note' => $note]);
Queue::assertPushed(SyndicateNoteToTwitter::class);
Queue::assertPushed(SyndicateNoteToMastodon::class); Queue::assertPushed(SyndicateNoteToMastodon::class);
} }
@ -248,10 +245,6 @@ class MicropubControllerTest extends TestCase
'path' => 'test-photo.jpg', 'path' => 'test-photo.jpg',
'type' => 'image', 'type' => 'image',
]); ]);
SyndicationTarget::factory()->create([
'uid' => 'https://twitter.com/jonnybarnes',
'service_name' => 'Twitter',
]);
SyndicationTarget::factory()->create([ SyndicationTarget::factory()->create([
'uid' => 'https://mastodon.social/@jonnybarnes', 'uid' => 'https://mastodon.social/@jonnybarnes',
'service_name' => 'Mastodon', 'service_name' => 'Mastodon',
@ -267,7 +260,6 @@ class MicropubControllerTest extends TestCase
'content' => [$note], 'content' => [$note],
'in-reply-to' => ['https://aaronpk.localhost'], 'in-reply-to' => ['https://aaronpk.localhost'],
'mp-syndicate-to' => [ 'mp-syndicate-to' => [
'https://twitter.com/jonnybarnes',
'https://mastodon.social/@jonnybarnes', 'https://mastodon.social/@jonnybarnes',
], ],
'photo' => [config('filesystems.disks.s3.url') . '/test-photo.jpg'], 'photo' => [config('filesystems.disks.s3.url') . '/test-photo.jpg'],
@ -279,7 +271,6 @@ class MicropubControllerTest extends TestCase
->assertStatus(201) ->assertStatus(201)
->assertJson(['response' => 'created']); ->assertJson(['response' => 'created']);
Queue::assertPushed(SendWebMentions::class); Queue::assertPushed(SendWebMentions::class);
Queue::assertPushed(SyndicateNoteToTwitter::class);
Queue::assertPushed(SyndicateNoteToMastodon::class); Queue::assertPushed(SyndicateNoteToMastodon::class);
} }

View file

@ -137,7 +137,6 @@ class MicropubMediaTest extends TestCase
); );
$sourceUploadResponse->assertJson(['items' => [[ $sourceUploadResponse->assertJson(['items' => [[
'url' => $response->headers->get('Location'), 'url' => $response->headers->get('Location'),
'published' => carbon()->toW3cString(),
'mime_type' => 'image/png', 'mime_type' => 'image/png',
]]]); ]]]);
@ -170,7 +169,6 @@ class MicropubMediaTest extends TestCase
); );
$sourceUploadResponse->assertJson(['items' => [[ $sourceUploadResponse->assertJson(['items' => [[
'url' => $response->headers->get('Location'), 'url' => $response->headers->get('Location'),
'published' => carbon()->toW3cString(),
'mime_type' => 'image/png', 'mime_type' => 'image/png',
]]]); ]]]);
// And given our limit of 1 there should only be one result // And given our limit of 1 there should only be one result

View file

@ -68,7 +68,7 @@ class NotesControllerTest extends TestCase
} }
/** @test */ /** @test */
public function unknownNoteGives404() public function unknownNoteGives404(): void
{ {
$response = $this->get('/notes/112233'); $response = $this->get('/notes/112233');
$response->assertNotFound(); $response->assertNotFound();

View file

@ -65,7 +65,7 @@ class HelpersTest extends TestCase
$this->assertEquals($expected, prettyPrintJson($json)); $this->assertEquals($expected, prettyPrintJson($json));
} }
public function urlProvider(): array public static function urlProvider(): array
{ {
return [ return [
['https://example.org/', 'https://example.org'], ['https://example.org/', 'https://example.org'],

View file

@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Jobs;
use App\Jobs\SyndicateBookmarkToTwitter;
use App\Models\Bookmark;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class SyndicateBookmarkToTwitterJobTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function weSendBookmarksToTwitter(): void
{
$faker = \Faker\Factory::create();
$randomNumber = $faker->randomNumber();
$json = json_encode([
'url' => 'https://twitter.com/' . $randomNumber,
]);
$mock = new MockHandler([
new Response(201, ['Content-Type' => 'application/json'], $json),
]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$bookmark = Bookmark::factory()->create();
$job = new SyndicateBookmarkToTwitter($bookmark);
$job->handle($client);
$this->assertDatabaseHas('bookmarks', [
'syndicates' => '{"twitter": "https://twitter.com/' . $randomNumber . '"}',
]);
}
}

View file

@ -1,40 +0,0 @@
<?php
namespace Tests\Unit\Jobs;
use App\Jobs\SyndicateNoteToTwitter;
use App\Models\Note;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class SyndicateNoteToTwitterJobTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function weSyndicateNotesToTwitter(): void
{
$faker = \Faker\Factory::create();
$randomNumber = $faker->randomNumber();
$json = json_encode([
'url' => 'https://twitter.com/i/web/status/' . $randomNumber,
]);
$mock = new MockHandler([
new Response(201, ['Content-Type' => 'application/json'], $json),
]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$note = Note::factory()->create();
$job = new SyndicateNoteToTwitter($note);
$job->handle($client);
$this->assertDatabaseHas('notes', [
'tweet_id' => $randomNumber,
]);
}
}

View file

@ -46,7 +46,15 @@ class NotesTest extends TestCase
public function defaultImageUsedAsFallbackInMakehcardsMethod(): void public function defaultImageUsedAsFallbackInMakehcardsMethod(): void
{ {
// phpcs:ignore // phpcs:ignore
$expected = '<p>Hi <span class="u-category h-card mini-h-card"><a class="u-url p-name" href="http://tantek.com">Tantek Çelik</a><span class="hovercard"> <a class="u-url" href="https://twitter.com/t"><img class="social-icon" src="/assets/img/social-icons/twitter.svg"> t</a><img class="u-photo" alt="" src="/assets/profile-images/default-image"></span></span></p>' . PHP_EOL; $expected = '<p>Hi <span class="u-category h-card mini-h-card">
<a class="u-url p-name" href="http://tantek.com">Tantek Çelik</a>
<span class="hovercard">
<img class="u-photo" alt="" src="/assets/profile-images/default-image">
<a class="u-url" href="https://twitter.com/t">
<img class="social-icon" src="/assets/img/social-icons/twitter.svg"> t
</a>
</span>
</span></p>' . PHP_EOL;
Contact::factory()->create([ Contact::factory()->create([
'nick' => 'tantek', 'nick' => 'tantek',
'name' => 'Tantek Çelik', 'name' => 'Tantek Çelik',
@ -84,7 +92,15 @@ class NotesTest extends TestCase
]); ]);
// phpcs:ignore // phpcs:ignore
$expected = '<p>Hi <span class="u-category h-card mini-h-card"><a class="u-url p-name" href="https://aaronparecki.com">Aaron Parecki</a><span class="hovercard"><a class="u-url" href="https://www.facebook.com/123456"><img class="social-icon" src="/assets/img/social-icons/facebook.svg"> Facebook</a> <img class="u-photo" alt="" src="/assets/profile-images/aaronparecki.com/image"></span></span></p>' . PHP_EOL; $expected = '<p>Hi <span class="u-category h-card mini-h-card">
<a class="u-url p-name" href="https://aaronparecki.com">Aaron Parecki</a>
<span class="hovercard">
<img class="u-photo" alt="" src="/assets/profile-images/aaronparecki.com/image">
<a class="u-url" href="https://www.facebook.com/123456">
<img class="social-icon" src="/assets/img/social-icons/facebook.svg"> Facebook
</a>
</span>
</span></p>' . PHP_EOL;
$this->assertEquals($expected, $note->note); $this->assertEquals($expected, $note->note);
} }

View file

@ -42,7 +42,7 @@ class TagsTest extends TestCase
$this->assertSame($expected, Tag::normalize($input)); $this->assertSame($expected, Tag::normalize($input));
} }
public function tagsProvider(): array public static function tagsProvider(): array
{ {
return [ return [
['test', 'test'], ['test', 'test'],

2
webpack.config.js vendored
View file

@ -53,7 +53,7 @@ module.exports = {
}), }),
new StyleLintPlugin({ new StyleLintPlugin({
configFile: path.resolve(__dirname + '/.stylelintrc'), configFile: path.resolve(__dirname + '/.stylelintrc'),
context: path.resolve(__dirname + '/resources/css'), context: path.resolve(__dirname + '/resources/css-2023'),
files: '**/*.css', files: '**/*.css',
}), }),
new EslintPlugin({ new EslintPlugin({