Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
c5bbcbaca3 Bump minimist from 1.2.5 to 1.2.6
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-09 03:20:44 +00:00
122 changed files with 15794 additions and 20791 deletions

33
.babelrc Normal file
View File

@@ -0,0 +1,33 @@
{
"presets": [
[
"@babel/preset-env", {
"targets": {
"browsers": [
"last 2 versions",
"ie >= 11"
]
},
"loose": true,
"modules": false
}
]
],
"env": {
"test": {
"presets": [
[
"@babel/preset-env", {
"targets": {
"browsers": [
"last 2 versions",
"ie >= 11"
]
},
"loose": true
}
]
]
}
}
}

View File

@@ -1,99 +1,128 @@
version: 2.1
orbs:
node: circleci/node@5.1
aws-s3: circleci/aws-s3@3.1
defaults: &defaults
working_directory: ~/tmp/windycoys
docker:
- image: circleci/node:10.15
semver-tags: &semver-tags
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/
develop: &develop
filters:
branches:
only: develop
other-branches: &other-branches
filters:
branches:
ignore:
- master
- develop
version: 2
jobs:
build_and_test:
executor: node/default
steps:
- checkout
- node/install-packages:
pkg-manager: npm
- run:
name: Run tests
command: npm run lint
- run:
name: Build app
command: npm run build
- persist_to_workspace:
root: ~/project
paths:
- .
deploy:
executor: node/default
install:
<<: *defaults
steps:
- attach_workspace:
at: ~/project
- run:
name: Known Hosts
command: |
mkdir ~/.ssh || :
echo -e $KNOWN_HOSTS > ~/.ssh/known_hosts
- run:
name: Clean
command: sh build/clean.sh
- run:
name: Deploy
command: sh build/deploy.sh
backup_to_s3:
docker:
- image: 'cimg/python:3.10'
steps:
at: ~/tmp/windycoys
- checkout
- run:
name: Known Hosts
command: |
mkdir ~/.ssh || :
echo -e $KNOWN_HOSTS > ~/.ssh/known_hosts
name: Install dependencies
command: npm install
- persist_to_workspace:
root: .
paths: .
test:
<<: *defaults
steps:
- attach_workspace:
at: ~/tmp/windycoys
- run:
name: Prepare filename
command: |
filename=windycoys-$(date +'%Y-%m-%dT%H-%M-%S').tgz
echo "export filename='$filename'" >> $BASH_ENV
name: Run tests
command: npm test
build:
<<: *defaults
environment:
NODE_ENV: production
steps:
- attach_workspace:
at: ~/tmp/windycoys
- run:
name: Prepare backups
command: |
ssh $SSH_USER@$SSH_IP "bash -c '\
name: Build CSS and JavaScript
command: npm run build
- run:
name: Generate timestamp
command: bash build/generate_timestamp.sh
- persist_to_workspace:
root: .
paths: .
deploy-prod:
<<: *defaults
steps:
- attach_workspace:
at: ~/tmp/windycoys
- run:
name: Install rsync
command: sudo apt install -y rsync
- run:
name: Deploy theme
command: bash build/deploy.sh prod
- run:
name: Purge Cloudflare cache
command: bash build/purge_cache.sh
deploy-develop:
<<: *defaults
steps:
- attach_workspace:
at: ~/tmp/windycoys
- run:
name: Install rsync
command: sudo apt install -y rsync
- run:
name: Deploy theme
command: bash build/deploy.sh develop
- run:
name: Purge Cloudflare cache
command: bash build/purge_cache.sh
source windycoys/.env
rm -rf windycoys/backup
mkdir -p windycoys/backup/data
scp -r windycoys/wp-content windycoys/backup/data/
docker exec -t $DOCKER_DB_NAME /usr/bin/mysqldump \
-uroot --password=$DB_ROOT_PASSWORD \
$DB_NAME | gzip -9 > windycoys/backup/data/database.sql.gz
/usr/bin/tar zcf windycoys/backup/windycoys.tgz windycoys/backup/data'"
- run:
name: Copy backup
command: |
scp $SSH_USER@$SSH_IP:~/windycoys/backup/windycoys.tgz ./
mv windycoys.tgz $filename
- aws-s3/copy:
aws-region: AWS_REGION
aws-access-key-id: AWS_ACCESS_KEY_ID
aws-secret-access-key: AWS_SECRET_ACCESS_KEY
from: '$filename'
to: 's3://$AWS_S3_BUCKET_NAME'
workflows:
test_my_app:
version: 2
test:
jobs:
- build_and_test:
filters:
branches:
only: main
- deploy:
- install: *other-branches
- test:
<<: *other-branches
requires:
- build_and_test
filters:
branches:
only: main
backup:
triggers:
- schedule:
cron: "0 2 * * 1"
filters:
branches:
only:
- next
- install
develop:
jobs:
- backup_to_s3
- install: *develop
- test:
<<: *develop
requires:
- install
- build:
<<: *develop
requires:
- test
- deploy-develop:
<<: *develop
requires:
- build
prod:
jobs:
- install: *semver-tags
- test:
<<: *semver-tags
requires:
- install
- build:
<<: *semver-tags
requires:
- test
- deploy-prod:
<<: *semver-tags
requires:
- build

View File

@@ -1,10 +0,0 @@
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
],
};

16
.eslintrc.json Normal file
View File

@@ -0,0 +1,16 @@
{
"plugins": [
"prettier"
],
"extends": [
"airbnb-base",
"prettier"
],
"env": {
"browser": true,
"jest": true
},
"rules": {
"prettier/prettier": "error"
}
}

46
.gitignore vendored
View File

@@ -1,5 +1,43 @@
.tmp
.old
# Temporary
.tmp/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# node-waf configuration
.lock-wscript
# Dependency directories
node_modules/
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Yarn Integrity file
.yarn-integrity
# Test
coverage/
# dotenv environment variables file
.env
dist
node_modules
# Distribution assets
theme/images
theme/bundle.js
theme/style.css
theme/.timestamp

View File

@@ -0,0 +1,3 @@
{
"trailingComma": "all"
}

13
.stylelintrc Normal file
View File

@@ -0,0 +1,13 @@
{
"plugins": [
"stylelint-prettier"
],
"extends": [
"stylelint-config-standard",
"stylelint-prettier/recommended"
],
"rules": {
"prettier/prettier": true,
"selector-pseudo-element-colon-notation": "single"
}
}

91
README.md Normal file
View File

@@ -0,0 +1,91 @@
<p align="center">
<img src="https://github.com/danhead/windycoys/raw/master/images/icon_144.png" title="WindyCOYS Logo">
</p>
<h1 align="center">Wordpress Theme</h1>
<p align="center">
<a href="https://windycoys.com" title="WindyCOYS Blog">
<img src="https://img.shields.io/website-up-down-green-red/https/windycoys.com.svg?label=windycoys.com"></a>
<a href="https://github.com/danhead/windycoys/releases" title="Theme releases">
<img src="https://img.shields.io/github/tag/danhead/windycoys.svg?label=version" alt="Latest version"></a>
<a href="https://circleci.com/gh/danhead/windycoys" title="CircleCI jobs">
<img src="https://img.shields.io/circleci/project/github/danhead/windycoys.svg?logo=circleci" alt="Latest build status"></a>
<img src="https://img.shields.io/david/danhead/windycoys.svg" alt="Theme dependencies">
<a href="https://github.com/danhead/windycoys/issues" title="Github issues">
<img src="https://img.shields.io/github/issues/danhead/windycoys.svg?logo=github" alt="Theme issues"></a>
<a href="https://github.com/danhead/windycoys/blob/master/LICENSE" title="Theme license">
<img src="https://img.shields.io/github/license/danhead/windycoys.svg" alt="Theme license"></a>
</p>
<p align="center">
<a href="https://twitter.com/intent/follow?screen_name=windycoys" title="Follow WindyCOYS on Twitter">
<img src="https://img.shields.io/twitter/follow/windycoys.svg?style=social&logo=twitter" alt="Follow WindyCOYS on Twitter"></a>
<a href="https://twitter.com/intent/follow?screen_name=danhead" title="Follow Dan Head on Twitter">
<img src="https://img.shields.io/twitter/follow/danhead.svg?style=social&logo=twitter" alt="Follow Dan Head on Twitter"></a>
</p>
## About
A modern Wordpress theme written for [WindyCOYS.com](https://windycoys.com).
Built using:
* [PostCSS](https://postcss.org)
* [Rollup.js](https://rollupjs.org) & [Babel](https://babeljs.io/)
## Requirements
* [Docker](https://www.docker.com/products/docker-desktop/)
* [Docker Compose](https://docs.docker.com/compose/install/)
* [Node.js](https://nodejs.org/)
## Building the theme
The build script runs the following tasks:
* **css:compile** - Process the CSS
* **js:compile** - Compile the JavaScript to ES5
* **images** - Minify images
The CSS and JavaScript will be minified if the environment variable `NODE_ENV='production'`.
```
npm run build
```
## Developing
The `start` script will monitor CSS, JavaScript and images for changes and automatically compile them.
It will also start Docker containers and proxy the connection through browser-sync.
```
npm start
```
### CSS
The CSS is compiled to `theme/style.css` using [postcss-preset-env](https://github.com/csstools/postcss-preset-env) which primarily used to resolve custom properties and add vendor prefixes. It is linted with [stylelint](https://stylelint.io) using [stylelint-selector-bem-pattern](https://github.com/simonsmith/stylelint-selector-bem-pattern).
### JavaScript
The JavaScript is transformed to ES5 and bundled to `theme/bundle.js`. It is linted with [eslint](https://eslint.org/) using [Airbnb's base config](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb-base).
Tests are written with [Jest](https://jestjs.io/) and run with:
```
npm run jest
```
The tests are written in `*.spec.js` files along side the source code.
To watch the files and automatically re-run the tests, use:
```
npm run jest:watch
```
## License
The WindyCOYS theme is licensed under the [MIT license](http://opensource.org/licenses/MIT)

View File

@@ -1 +0,0 @@
node_modules

View File

@@ -1,6 +0,0 @@
FROM node:18
WORKDIR /usr/src/app
COPY . .
RUN npm ci
EXPOSE 3000
CMD ["node", "index.js"]

View File

@@ -1,20 +0,0 @@
const bs = require("browser-sync").create();
bs.watch("dist/styles.css", (e) => {
if (e === "change") {
bs.reload("styles.css");
}
});
bs.watch("dist/bundle.js", (e) => {
if (e === "change") {
bs.reload("bundle.js");
}
});
bs.watch("dist/**/*.php", () => bs.reload());
bs.init({
open: false,
proxy: "http://windycoys_wordpress",
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
{
"name": "windycoys-browser-sync",
"version": "1.0.0",
"private": true,
"description": "Browsersync Docker container for local developement",
"main": "index.js",
"license": "ISC",
"dependencies": {
"browser-sync": "^2.27.9"
}
}

View File

@@ -1,17 +0,0 @@
#!/usr/bin/env sh
source .env
rm -rf ~/windycoys/backup
rm -f ~/windycoys/windycoys-*.tgz
mkdir -p ~/windycoys/backup
scp -r ~/windycoys/wp-content ~/windycoys/backup/
docker exec -it $DOCKER_DB_NAME /usr/bin/mysqldump -uroot -p"$DB_ROOT_PASSWORD" $DB_NAME | gzip -9 > ~/windycoys/backup/database.sql.gz
ls -l ~/windycoys/backup/
datetime=$(date +'%Y-%m-%dT%H-%M-%S')
file=windycoys-$datetime.tgz
/usr/bin/tar zcf ~/windycoys/$file ~/windycoys/backup
rm -rf ~/windycoys/backup

View File

@@ -1,8 +0,0 @@
#!/usr/bin/env sh
# Test environment variables are set
[ -z "$SSH_IP" ] && { echo >&2 "Error: SSH_IP is missing. Aborting."; exit 1; }
[ -z "$SSH_USER" ] && { echo >&2 "Error: SSH_USER is missing. Aborting."; exit 1; }
# Deploy theme
ssh $SSH_USER@$SSH_IP rm -r ~/windycoys/dist/*

14
build/deploy.sh Normal file → Executable file
View File

@@ -1,9 +1,13 @@
#!/usr/bin/env sh
#!/usr/bin/env bash
# Test environment variables are set
# Ensure target is defined
[ -z "$1" ] && { echo >&2 "Error: Build target not defined. Aborting."; exit 1; }
# Test rsync is installed and ENV variables are set
hash rsync 2>/dev/null || { echo >&2 "Error: rsync is not installed. Aborting."; exit 1; }
[ -z "$SSH_IP" ] && { echo >&2 "Error: SSH_IP is missing. Aborting."; exit 1; }
[ -z "$SSH_USER" ] && { echo >&2 "Error: SSH_USER is missing. Aborting."; exit 1; }
# Deploy theme
scp ./build/docker-compose.yml $SSH_USER@$SSH_IP:~/windycoys/
scp -r ./dist $SSH_USER@$SSH_IP:~/windycoys/
# Deploy theme with rsync
rsync -e "ssh -o StrictHostKeyChecking=no" -avzuh --progress \
theme/. $SSH_USER@$SSH_IP:~/windycoys_theme_$1

View File

@@ -1,36 +0,0 @@
version: "3.8"
services:
db:
image: mariadb:10.8
container_name: windycoys_db
volumes:
- ../.tmp/db:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=${dbRootPassword}
- MYSQL_PASSWORD=${dbPassword}
- MYSQL_DATABASE=${dbDatabase}
- MYSQL_USER=${dbUser}
restart: unless-stopped
wordpress:
image: wordpress:latest
container_name: windycoys_wordpress
environment:
- WORDPRESS_DB_HOST=windycoys_db
- WORDPRESS_DB_USER=${dbUser}
- WORDPRESS_DB_PASSWORD=${dbPassword}
- WORDPRESS_DB_NAME=wordpress
- WORDPRESS_TABLE_PREFIX=wp_
- WORDPRESS_DEBUG=1
volumes:
- ../.tmp/wp-content:/var/www/html/wp-content
- ../dist:/var/www/html/wp-content/themes/windycoys
restart: unless-stopped
depends_on:
- db
browser_sync:
image: windycoys-browser-sync:latest
container_name: windycoys_browser_sync
volumes:
- ../dist:/usr/src/app/dist
ports:
- "3000:3000"

View File

@@ -1,28 +1,22 @@
version: "3.8"
services:
db:
image: mariadb:10.8
container_name: windycoys_db
volumes:
- ./.tmp/db:/var/lib/mysql
wordpress:
environment:
- MYSQL_ROOT_PASSWORD=${dbRootPassword}
- MYSQL_PASSWORD=${dbPassword}
- MYSQL_DATABASE=${dbDatabase}
- MYSQL_USER=${dbUser}
restart: unless-stopped
wordpress:
image: wordpress:latest
container_name: windycoys_wordpress
environment:
- WORDPRESS_DB_HOST=windycoys_db
- WORDPRESS_DB_USER=${dbUser}
- WORDPRESS_DB_PASSWORD=${dbPassword}
- WORDPRESS_DB_NAME=wordpress
- WORDPRESS_TABLE_PREFIX=wp_
volumes:
- ./.tmp/wp-content:/var/www/html/wp-content
- ./dist:/var/www/html/wp-content/themes/windycoys
restart: unless-stopped
depends_on:
- WORDPRESS_DB_HOST=db
- WORDPRESS_DB_PASSWORD=password
- WORDPRESS_DB_USER=windycoys
image: 'wordpress:php7.0-apache'
links:
- db
ports:
- "80:80"
volumes:
- ../.tmp/wp-content:/var/www/html/wp-content
- ../theme:/var/www/html/wp-content/themes/windycoys
db:
environment:
- MYSQL_DATABASE=wordpress
- MYSQL_PASSWORD=password
- MYSQL_ROOT_PASSWORD=password
- MYSQL_USER=windycoys
image: 'mariadb:latest'
volumes:
- ../.tmp/mysql:/var/lib/mysql

5
build/generate_timestamp.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# Write deploy timestamp to disk
timestamp=$(date +%s)
echo $timestamp > theme/.timestamp

34
build/imagemin.js Normal file
View File

@@ -0,0 +1,34 @@
const { writeFile } = require("fs");
const { resolve } = require("path");
const imagemin = require("imagemin");
const imageminPngquant = require("imagemin-pngquant");
const imageminGifsicle = require("imagemin-gifsicle");
const imageminJpegtran = require("imagemin-jpegtran");
imagemin(["images/*.{png,jpg,gif}"], "theme/images", {
plugins: [
imageminPngquant({
quality: [0.65, 0.8],
}),
imageminGifsicle({
optimizationLevel: 3,
}),
imageminJpegtran(),
],
})
.then(files => {
files.forEach(file => {
const path = resolve(__dirname, "../", file.path);
writeFile(path, file.data, error => {
if (error) {
process.stdout.write(`${error}\n`);
process.exit(1);
}
process.stdout.write(`File written: ${path}\n`);
});
});
})
.catch(error => {
process.stderr.write(`${error}\n`);
process.exit(1);
});

1
build/purge_cache.sh Normal file → Executable file
View File

@@ -12,3 +12,4 @@ curl -X DELETE \
-H "X-Auth-Email: $CLOUDFLARE_EMAIL" \
--data '{"purge_everything":true}' \
"https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_IDENTIFIER/purge_cache"

99
build/serve.js Normal file
View File

@@ -0,0 +1,99 @@
const { resolve } = require("path");
const { execSync } = require("child_process");
const { upAll, stop } = require("docker-compose");
const browsersync = require("browser-sync");
const mysql = require("mysql");
const name = "windycoys";
const dockerOptions = {
cwd: resolve(__dirname),
log: true,
};
const browserSyncOptions = {
files: ["theme/**/*"],
watchtask: true,
open: false,
};
const mysqlConfig = {
user: "root",
password: "password",
database: "wordpress",
};
process.env.COMPOSE_PROJECT_NAME = name;
// Get IP address
const getIpAddr = function getIpAddr(id) {
return execSync(
`docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ${id}`,
)
.toString("utf8")
.trim();
};
// Update URIs in MYSQL database
const updateUriData = function updateUriData(wordpress, db) {
mysqlConfig.host = db;
const con = mysql.createConnection(mysqlConfig);
con.connect();
const query = `UPDATE wp_options SET option_value='http://${wordpress}' WHERE option_name in ('siteurl', 'home')`;
con.query(query, error => {
if (error) {
process.stderr.write(`Error: ${error.sqlMessage}\n`);
process.exit(1);
}
});
con.end();
};
// Register Exit handler to stop Docker containers on exit
const exitHandler = function exitHandler() {
stop(dockerOptions).then(
() => {
process.stdout.write("\n");
process.exit();
},
err => {
process.stderr.write(`Error: ${err.message}\n`);
process.exit(1);
},
);
};
process.on("beforeExit", exitHandler.bind());
process.on("SIGINT", exitHandler.bind());
process.on("SIGUSR1", exitHandler.bind());
process.on("SIGUSR2", exitHandler.bind());
process.on("uncaughtException", exitHandler.bind());
// Start Docker containers
upAll(dockerOptions).then(
() => {
process.stdout.write(
"Docker containers launched, starting browser-sync...",
);
const wordpress = getIpAddr(`${name}_wordpress_1`);
const db = getIpAddr(`${name}_db_1`);
if (wordpress && db) {
updateUriData(wordpress, db);
const bs = browsersync.create();
browserSyncOptions.proxy = wordpress;
bs.init(browserSyncOptions);
} else {
process.stderr.write(
"Error: No IP address for Docker containers. Aborting.\n",
);
process.exit(1);
}
},
err => {
process.stderr.write(`Error: ${err.message}\n`);
process.exit(1);
},
);

View File

@@ -1,27 +1,38 @@
@import "normalize";
@import "base";
@import "archive";
@import "article";
@import "avatar";
@import "comment-respond";
@import "comment";
@import "comments";
@import "content";
@import "excerpt";
@import "footer";
@import "icon";
@import "link";
@import "logo";
@import "menu";
@import "metadata";
@import "nav";
@import "page";
@import "pagination";
@import "search";
@import "silhouette";
@import "nav";
@import "menu";
@import "logo";
@import "social";
@import "icon";
@import "metadata";
@import "search";
@import "article";
@import "content";
@import "footer";
@import "comments";
@import "comment";
@import "comment-respond";
@import "avatar";
@import "link";
@import "pagination";
@import "title";
@import "archive";
@import "excerpt";
@import "silhouette";
body {
:root {
--color-black: #000;
--color-near-black: #333;
--color-dark-blue: #201f42;
--color-light-blue: #1574d2;
--color-pale-blue: #e9f4ff;
--color-grey: #757575;
--color-light-grey: #e0e0e0;
--color-facebook: #3e5a98;
--color-twitter: #1da1f2;
--color-rss: #ee802f;
--space-xxs: 0.25rem;
--space-xs: 0.5rem;
--space-sm: 1rem;
@@ -55,19 +66,6 @@ body {
--transition-sl: 0.6s;
--heading-font-family: "Raleway", sans-serif;
--body-font-family: "Open Sans", sans-serif;
}
.theme-light {
--color-black: #000;
--color-near-black: #333;
--color-dark-blue: #201f42;
--color-light-blue: #1574d2;
--color-pale-blue: #e9f4ff;
--color-grey: #757575;
--color-light-grey: #e0e0e0;
--color-facebook: #3e5a98;
--color-twitter: #1da1f2;
--color-rss: #ee802f;
--link-focus: 0 0.1rem 0 0 var(--color-light-blue);
--image-focus: 0 0 0 0.2rem #fff, 0 0 0 0.4rem var(--color-light-blue);
--bullet-size: 0.45rem;

2
css/normalize.css vendored Normal file
View File

@@ -0,0 +1,2 @@
/* stylelint-disable at-rule-no-unknown */
@import-normalize;

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 162 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 236 KiB

View File

Before

Width:  |  Height:  |  Size: 444 B

After

Width:  |  Height:  |  Size: 444 B

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

Before

Width:  |  Height:  |  Size: 971 KiB

After

Width:  |  Height:  |  Size: 971 KiB

View File

@@ -1,7 +0,0 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
roots: ["src/ts"],
};

26
js/image.js Normal file
View File

@@ -0,0 +1,26 @@
export default class Image {
constructor(image) {
this.image = image;
this.dataset = image.dataset;
}
fadeIn() {
this.image.style.opacity = 1;
}
start() {
if (this.dataset.loadTransition === "false") {
return;
}
this.image.style.opacity = 0;
if (this.image.complete) {
this.fadeIn();
} else {
this.attachLoadedEvent();
}
}
attachLoadedEvent() {
this.image.addEventListener("load", () => this.fadeIn());
}
}

68
js/image.spec.js Normal file
View File

@@ -0,0 +1,68 @@
import Image from "./image";
const imageHtml = `
<img class="image" src="foo.jpg"></img>
<img class="disabled-image" data-load-transition="false" src="foo.jpg"></img>
`;
describe("Image", () => {
let image;
let disabledImage;
beforeEach(() => {
document.body.innerHTML = imageHtml;
image = document.querySelector(".image");
disabledImage = document.querySelector(".disabled-image");
});
it("Should initialize", () => {
const img = new Image(image);
expect(img).toBeInstanceOf(Image);
});
it("Should store a reference to the image element", () => {
const img = new Image(image);
expect(img.image).toBe(image);
});
test("fadeIn should set the element opacity to 1", () => {
const img = new Image(image);
img.fadeIn();
expect(img.image.style.opacity).toBe("1");
});
test("start should set the element opacity to 0", () => {
const img = new Image(image);
img.start();
expect(img.image.style.opacity).toBe("0");
});
test("start should call fadeIn when image has already loaded", () => {
const img = new Image(image);
img.fadeIn = jest.fn();
Object.defineProperty(img.image, "complete", {
value: true,
writable: false,
});
img.start();
expect(img.fadeIn).toHaveBeenCalledTimes(1);
});
test("start should call attachLoadedEvent if image has not yet loaded", () => {
const img = new Image(image);
img.attachLoadedEvent = jest.fn();
Object.defineProperty(img.image, "complete", {
value: false,
writable: false,
});
img.start();
expect(img.attachLoadedEvent).toHaveBeenCalledTimes(1);
});
test("start should do nothing if data-load-transition=false", () => {
const img = new Image(disabledImage);
img.attachLoadedEvent = jest.fn();
img.start();
expect(img.attachLoadedEvent).toHaveBeenCalledTimes(0);
});
});

3
js/index.js Normal file
View File

@@ -0,0 +1,3 @@
import windy from "./windy";
document.addEventListener("DOMContentLoaded", windy);

59
js/menu.js Normal file
View File

@@ -0,0 +1,59 @@
const STATES = {
visible: "is-visible",
minimized: "is-minimized",
};
export default class Menu {
constructor(cfg = {}) {
if (!cfg.container) return false;
this.container = cfg.container;
this.state = "visible";
this.lastKnownScrollPosition = 0;
}
init() {
if (!this.state) return false;
this.container.setAttribute("aria-haspopup", true);
this.container.setAttribute("aria-expanded", false);
this.attachEvents();
setTimeout(() => {
this.showMenu();
}, 1000);
return true;
}
attachEvents() {
window.addEventListener("scroll", () => {
this.updateState(this.lastKnownScrollPosition, window.scrollY);
this.lastKnownScrollPosition = window.scrollY;
});
}
updateState(lastY = 0, currentY) {
if (this.state !== "hidden") {
if (this.state !== "visible" && currentY < lastY) {
this.showMenu();
} else if (this.state !== "minimized" && currentY > lastY) {
this.minimizeMenu();
}
}
}
showMenu() {
this.state = "visible";
this.container.setAttribute("aria-expanded", false);
this.container.classList.add(STATES.visible);
this.container.classList.remove(STATES.minimized);
}
minimizeMenu() {
this.state = "minimized";
this.container.classList.add(STATES.minimized);
}
hideMenu() {
this.state = "hidden";
this.container.setAttribute("aria-expanded", true);
this.container.classList.remove(STATES.visible);
}
}

114
js/menu.spec.js Normal file
View File

@@ -0,0 +1,114 @@
import Menu from "./menu";
jest.useFakeTimers();
const menuHtml = `
<button class="menu">
<span class="menu__line"></span>
<span class="menu__line"></span>
<span class="menu__line"></span>
<span class="menu__text">Open menu</span>
</button>`;
describe("Menu button", () => {
let menu;
beforeEach(() => {
document.body.innerHTML = menuHtml;
menu = new Menu({
container: document.querySelector(".menu"),
});
});
it("Should initialise", () => {
menu.init();
expect(menu).toMatchObject({});
});
it("Should fail to initialise if container is missing", () => {
menu.init();
expect(new Menu().state).toBeUndefined();
});
it("Should default to visible state", () => {
expect(menu.state).toBe("visible");
});
it("Should be hidden initially", () => {
menu.init();
expect(menu.container.classList.contains("is-visible")).toBe(false);
});
it("Should be visible after 1 second", () => {
menu.init();
jest.runAllTimers();
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
expect(menu.container.classList.contains("is-visible")).toBe(true);
});
test("Menu can be shown", () => {
menu.init();
menu.showMenu();
expect(menu.state).toBe("visible");
expect(menu.container.classList.contains("is-visible")).toBe(true);
expect(menu.container.classList.contains("is-minimized")).toBe(false);
});
test("Menu can be hidden", () => {
menu.init();
menu.hideMenu();
expect(menu.state).toBe("hidden");
expect(menu.container.classList.contains("is-visible")).toBe(false);
expect(menu.container.classList.contains("is-minimized")).toBe(false);
});
test("Menu can be minimized", () => {
menu.init();
menu.minimizeMenu();
expect(menu.state).toBe("minimized");
expect(menu.container.classList.contains("is-visible")).toBe(false);
expect(menu.container.classList.contains("is-minimized")).toBe(true);
});
test("The state should not change on scroll if it is hidden", () => {
menu.init();
menu.hideMenu();
menu.updateState(10, 0);
expect(menu.state).toBe("hidden");
menu.updateState(0, 10);
expect(menu.state).toBe("hidden");
});
test("The state should change to minimized when scrolling down", () => {
menu.init();
menu.showMenu();
menu.updateState(0, 10);
expect(menu.state).toBe("minimized");
});
test("The state should change to visible when scrolling up", () => {
menu.init();
menu.minimizeMenu();
menu.updateState(10, 0);
expect(menu.state).toBe("visible");
});
it("Should have initial ARIA attributes", () => {
menu.init();
expect(menu.container.getAttribute("aria-haspopup")).toBe("true");
expect(menu.container.getAttribute("aria-expanded")).toBe("false");
});
it("Should have updated ARIA attribute when menu is opened", () => {
menu.init();
menu.showMenu();
expect(menu.container.getAttribute("aria-expanded")).toBe("false");
});
it("Should have updated ARIA attribute when menu is closed", () => {
menu.init();
menu.showMenu();
menu.hideMenu();
expect(menu.container.getAttribute("aria-expanded")).toBe("true");
});
});

78
js/navigation.js Normal file
View File

@@ -0,0 +1,78 @@
const STATES = {
open: "is-open",
};
export default class Navigation {
constructor(cfg = {}) {
if (!cfg.container) return false;
if (!cfg.menu) return false;
this.state = "hidden";
this.container = cfg.container;
this.menu = cfg.menu;
}
init() {
if (!this.state) return false;
this.container.setAttribute("aria-hidden", true);
this.attachClickEvents();
this.attachKeyEvents();
return true;
}
attachClickEvents() {
this.container.addEventListener("click", e => {
if (this.container === e.target) {
this.closeNav();
}
});
this.menu.container.addEventListener("click", () => {
this.openNav();
});
this.container
.querySelector(".nav__close")
.addEventListener("click", () => {
this.closeNav();
});
}
attachKeyEvents() {
const first = this.container.querySelector(".nav__link");
const last = this.container.querySelector(".nav__close");
this.container.addEventListener("keydown", e => {
if (e.keyCode === 27) {
this.closeNav();
}
});
first.addEventListener("keydown", e => {
if (e.shiftKey && e.keyCode === 9) {
e.preventDefault();
last.focus();
}
});
last.addEventListener("keydown", e => {
if (!e.shiftKey && e.keyCode === 9) {
e.preventDefault();
first.focus();
}
});
}
openNav() {
this.lastActiveElement = document.activeElement;
this.state = "visible";
this.menu.hideMenu();
this.container.classList.add(STATES.open);
this.container.setAttribute("aria-hidden", false);
this.container.querySelector(".nav__title-link").focus();
}
closeNav() {
this.state = "hidden";
this.menu.showMenu();
this.container.classList.remove(STATES.open);
this.container.setAttribute("aria-hidden", true);
if (this.lastActiveElement) {
this.lastActiveElement.focus();
}
}
}

167
js/navigation.spec.js Normal file
View File

@@ -0,0 +1,167 @@
import Navigation from "./navigation";
const navHtml = `<a href="/test">Test link</a>
<nav class="nav">
<button class="menu">Menu</button>
<div class="nav__content">
<h2 class="nav__title">
<a class="nav__title-link" href="/">WindyCOYS</a>
</h2>
<ul class="nav__list">
<li class="nav__list-item">
<a class="nav__link" href="/">Home</a>
</li>
<li class="nav__list-item">
<a class="nav__link" href="/about">About</a>
</li>
</ul>
</div>
<button class="nav__close">close</button>
</nav>
<a href="/another-test">Another test link</a>`;
let nav;
beforeEach(() => {
document.body.innerHTML = navHtml;
// Mock Menu
const menu = {
container: document.querySelector(".menu"),
showMenu: jest.fn(),
hideMenu: jest.fn(),
};
nav = new Navigation({
container: document.querySelector(".nav"),
menu,
});
});
describe("Navigation", () => {
it("Should initialise", () => {
nav.init();
expect(nav).toMatchObject({});
});
it("Should fail to initialise if container is missing", () => {
nav.init();
expect(new Navigation().state).toBeUndefined();
});
it("Should fail to initialise if the menu is not provided", () => {
const noMenu = new Navigation({
container: document.querySelector(".nav"),
});
noMenu.init();
expect(noMenu.state).toBeUndefined();
});
it("Should be hidden by default", () => {
nav.init();
expect(nav.container.classList.contains("is-open")).toBe(false);
});
it("Should close when the .nav element is clicked", () => {
nav.init();
nav.openNav();
const event = new Event("click");
nav.container.dispatchEvent(event);
expect(nav.state).toBe("hidden");
});
it("Should close when the user presses ESC", () => {
nav.init();
nav.openNav();
const event = new Event("keydown");
event.keyCode = 27;
nav.container.dispatchEvent(event);
expect(nav.state).toBe("hidden");
});
it("Should not close when the .nav__content element is clicked", () => {
nav.init();
nav.openNav();
const event = new Event("click");
nav.container.querySelector(".nav__content").dispatchEvent(event);
expect(nav.state).toBe("visible");
});
it("Should have initial ARIA attributes", () => {
nav.init();
expect(nav.container.getAttribute("aria-hidden")).toBe("true");
});
it("Should update the ARIA attributes when visible", () => {
nav.init();
nav.openNav();
expect(nav.container.getAttribute("aria-hidden")).toBe("false");
});
it("Should update the ARIA attributes when hidden", () => {
nav.init();
nav.openNav();
nav.closeNav();
expect(nav.container.getAttribute("aria-hidden")).toBe("true");
});
});
describe("Navigation menu button", () => {
it("Should open the Navigation when clicked", () => {
nav.init();
const event = new Event("click");
nav.menu.container.dispatchEvent(event);
expect(nav.state).toBe("visible");
});
});
describe("Navigation close button", () => {
it("Should close the Navigation when clicked", () => {
nav.init();
nav.openNav();
const event = new Event("click");
nav.container.querySelector(".nav__close").dispatchEvent(event);
expect(nav.state).toBe("hidden");
});
});
describe("Navigation focus behaviour", () => {
let first;
let last;
let menu;
beforeEach(() => {
menu = nav.container.querySelector(".menu");
first = nav.container.querySelector(".nav__link");
last = nav.container.querySelector(".nav__close");
menu.focus();
nav.init();
nav.openNav();
});
it("Should apply focus to .nav__title-link when opened", () => {
const link = nav.container.querySelector(".nav__title-link");
expect(document.activeElement).toBe(link);
});
test("Focus moves to first element when TAB pressed on last", () => {
last.focus();
const event = new Event("keydown");
event.keyCode = 9;
last.dispatchEvent(event);
expect(document.activeElement).toBe(first);
});
test("Focus moves to last element when SHIFT + TAB pressed on first", () => {
first.focus();
const event = new Event("keydown");
event.shiftKey = true;
event.keyCode = 9;
first.dispatchEvent(event);
expect(document.activeElement).toBe(last);
});
test("Focus is restored to last active element when Navigation is closed", () => {
first.focus();
nav.closeNav();
expect(document.activeElement).toBe(menu);
});
});

51
js/reply-to-comment.js Normal file
View File

@@ -0,0 +1,51 @@
export default class ReplyToComment {
constructor(cfg = {}) {
if (!cfg.form || cfg.form.className !== "comments__foot") {
return false;
}
if (!cfg.links || cfg.links.length === 0) {
return false;
}
this.form = cfg.form;
this.backupForm = cfg.form.querySelector("#respond").cloneNode(true);
this.links = Array.prototype.slice.call(cfg.links);
this.links.forEach(link => {
link.addEventListener("click", e => this.handleLinkClick(e, link));
});
}
insertForm(container, id, author) {
const form = this.backupForm.cloneNode(true);
const cancel = form.querySelector("#cancel-comment-reply-link");
form.querySelector(
".comment-respond__title-text",
).innerHTML = `Reply to ${author}`;
form.querySelector("#submit").value = "Post reply";
form.querySelector("#comment_parent").value = id;
cancel.style.display = "inline-block";
cancel.addEventListener("click", e => this.handleCancelClick(e, container));
container.appendChild(form);
}
handleCancelClick(e, container) {
e.preventDefault();
container.parentNode
.querySelector(".comment__reply")
.classList.remove("is-hidden");
while (container.firstChild) {
container.removeChild(container.firstChild);
}
this.form.appendChild(this.backupForm);
}
handleLinkClick(e, link) {
e.preventDefault();
link.classList.add("is-hidden");
while (this.form.firstChild) {
this.form.removeChild(this.form.firstChild);
}
const container = link.parentNode.querySelector(".comment__inline-reply");
const { commentId, commentAuthor } = link.dataset;
this.insertForm(container, commentId, commentAuthor);
}
}

142
js/reply-to-comment.spec.js Normal file
View File

@@ -0,0 +1,142 @@
import ReplyToComment from "./reply-to-comment";
const commentHtml = `
<div class="comment">
<div class="comment__head">
<div class="comment__avatar"></div>
<div class="comment__metadata">
<span class="comment__author">Mr Smith</span>
<span class="comment__date">21st September, 2018 at 15:36</span>
</div>
</div>
<div class="comment__body">This is a comment.</div>
<div class="comment__foot">
<a class="comment__reply" data-comment-author="Mr Smith" data-comment-id="1234" href="?replytocom=1234#respond">
<span class="comment__reply-text">Reply</span>
</a>
<div class="comment__inline-reply"></div>
</div>
</div>`;
const formHtml = `
<div class="comments__foot">
<div id="respond" class="comment-respond">
<h3 id="reply-title" class="comment-respond__title">
<span class="comment-respond__title-text">
Leave a Reply
</span>
<div class="comment-respond__cancel">
<a rel="nofollow" id="cancel-comment-reply-link" href="#respond">Cancel reply</a>
</div>
</h3>
<form action="/wp-comments-post.php" method="post" id="commentform" class="comment-respond__form">
<p class="comment-respond__notes">Your email address will not be published.</p>
<label class="comment-respond__label" for="comment">Comment</label>
<textarea class="comment-respond__input" id="comment" name="comment" cols="25" rows="8" aria-required="true"></textarea>
<div class="comment-respond__item">
<label class="comment-respond__label" for="author">Name</label>
<input class="comment-respond__input" id="author" name="author" type="text" value="" size="30">
</div>
<div class="comment-respond__item">
<label class="comment-respond__label" for="email">Email</label>
<input class="comment-respond__input id="email" name="email" type="text" value="" size="30">
</div>
<div class="comment-respond__item">
<label class="comment-respond__label" for="url">Website</label>
<input class="comment-respond__input" id="url" name="url" type="text" value="" size="30">
</div>
<p class="form-submit">
<input name="submit" type="submit" id="submit" class="comment-respond__submit" value="Post Comment">
<input type="hidden" name="comment_post_ID" value="2683" id="comment_post_ID">
<input type="hidden" name="comment_parent" id="comment_parent" value="0">
</p>
<p style="display: none;">
<input type="hidden" id="akismet_comment_nonce" name="akismet_comment_nonce" value="123456789">
</p>
<input type="hidden" id="ak_js" name="ak_js" value="987654321">
</form>
</div>
</div>
`;
let comments;
let form;
let links;
let inlineReply;
beforeEach(() => {
document.body.innerHTML = `${commentHtml}${formHtml}`;
form = document.querySelector(".comments__foot");
links = document.querySelectorAll(".comment__reply");
inlineReply = document.querySelector(".comment__inline-reply");
comments = new ReplyToComment({ form, links });
});
describe("ReplyToComment", () => {
it("Should initialise", () => {
expect(comments.form).toBeInstanceOf(HTMLDivElement);
expect(comments.links).toBeInstanceOf(Array);
});
it("Should fail to initialise if form is missing", () => {
expect(new ReplyToComment()).toEqual({});
});
it("Should fail to initialise if there are no links", () => {
expect(
new ReplyToComment({
form: document.querySelector("#respond"),
}),
).toEqual({});
expect(
new ReplyToComment({
form: document.querySelector("#respond"),
links: document.querySelectorAll(".no-links-here"),
}),
).toEqual({});
});
it("Should remove the form from the DOM when a reply link is clicked", () => {
const event = new Event("click");
comments.handleLinkClick(event, links[0]);
expect(form.innerHTML).toEqual("");
});
it("Should hide the link when it is clicked", () => {
const event = new Event("click");
comments.handleLinkClick(event, links[0]);
expect(links[0].classList.contains("is-hidden")).toBe(true);
});
it("Should insert the form into the inline reply container", () => {
comments.insertForm(inlineReply);
const inlineForm = inlineReply.querySelectorAll("form");
expect(inlineForm.length).toEqual(1);
});
it("Should update the comment_parent hidden element", () => {
comments.insertForm(inlineReply, "123", "Mr Smith");
const { value } = inlineReply.querySelector("#comment_parent");
expect(value).toEqual("123");
});
it("Should add a cancel link to the inline form", () => {
comments.insertForm(inlineReply);
const el = inlineReply.querySelector("#cancel-comment-reply-link");
expect(el.style.display).toEqual("inline-block");
});
it("Should restore the reply link when the cancel link is clicked", () => {
const event = new Event("click");
comments.insertForm(inlineReply);
comments.handleCancelClick(event, inlineReply);
expect(links[0].classList.contains("is-hidden")).toBe(false);
});
it("Should remove the inline form when the cancel link is clicked", () => {
const event = new Event("click");
comments.insertForm(inlineReply);
comments.handleCancelClick(event, inlineReply);
expect(inlineReply.innerHTML).toEqual("");
});
});

31
js/windy.js Normal file
View File

@@ -0,0 +1,31 @@
import Menu from "./menu";
import Navigation from "./navigation";
import Image from "./image";
import ReplyToComment from "./reply-to-comment";
const windy = {
images: [],
};
export default function init() {
Array.prototype.slice
.call(document.querySelectorAll("img"))
.forEach(image => {
const img = new Image(image);
img.start();
windy.images.push(img);
});
windy.menu = new Menu({
container: document.querySelector(".menu"),
});
windy.navigation = new Navigation({
container: document.querySelector(".nav"),
menu: windy.menu,
});
windy.menu.init();
windy.navigation.init();
windy.comments = new ReplyToComment({
form: document.querySelector(".comments__foot"),
links: document.querySelectorAll(".comment__reply"),
});
}

29873
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "windycoys",
"version": "2.0.0-alpha1",
"version": "1.0.1",
"description": "Wordpress theme for windycoys.com",
"author": "Dan Head <me@danielhead.com>",
"license": "MIT",
@@ -9,55 +9,98 @@
"url": "https://github.com/danhead/windycoys"
},
"scripts": {
"browser-sync": "docker build -t windycoys-browser-sync browser-sync",
"build": "run-p build:*",
"build:css": "cross-env NODE_ENV=production postcss src/index.css -o dist/style.css",
"build:files": "copyfiles -u 1 'src/**/*.{json,php,jpg,png,gif}' dist",
"build:ts": "cross-env NODE_ENV=production webpack",
"lint": "run-p lint:*",
"lint:eslint": "eslint src --ext .ts",
"postinstallf": "run-p browser-sync",
"serve": "docker-compose -f build/docker-compose.dev.yml up --force-recreate 2>/dev/null",
"start": "run-p serve watch",
"test": "jest",
"build": "run-s images css:compile js:compile",
"css": "run-p -c css:*",
"css:compile": "postcss -o theme/style.css css/index.css",
"css:lint": "stylelint css/*.css",
"images": "node build/imagemin.js",
"jest": "jest",
"jest:watch": "jest --watch",
"js": "run-s js:*",
"js:compile": "rollup --config rollup.config.js",
"js:lint": "eslint build js",
"serve": "node build/serve.js",
"start": "run-p watch serve",
"test": "run-s css:lint js:lint jest",
"watch": "run-p watch:*",
"watch:css": "postcss src/index.css -o dist/styles.css -w",
"watch:ts": "webpack -w",
"watch:files": "copy-and-watch --watch 'src/**/*.{json,php,jpg,png,gif}' dist/"
"watch:css": "nodemon -q --watch css -e css --exec npm run css",
"watch:images": "nodemon -q --watch images -e png,jpg,gif --exec npm run images",
"watch:js": "nodemon -q --watch js -e js --exec npm run js"
},
"dependencies": {
"@babel/cli": "^7.14.8",
"@babel/core": "^7.10.3",
"@babel/preset-env": "^7.10.3",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^26.1.0",
"browser-sync": "^2.27.5",
"cssnano": "^4.1.10",
"docker-compose": "^0.12.0",
"eslint": "^5.16.0",
"eslint-config-airbnb-base": "^13.2.0",
"eslint-config-prettier": "^4.3.0",
"eslint-plugin-import": "^2.21.2",
"eslint-plugin-prettier": "^3.1.4",
"husky": "^1.3.1",
"imagemin": "^6.1.0",
"imagemin-gifsicle": "^6.0.1",
"imagemin-jpegtran": "^6.0.0",
"imagemin-pngquant": "^7.0.0",
"jest": "^26.1.0",
"lint-staged": "^8.2.1",
"mysql": "^2.18.1",
"nodemon": "^2.0.4",
"npm-run-all": "^4.1.5",
"postcss-banner": "^3.0.2",
"postcss-cli": "^7.1.1",
"postcss-import": "^12.0.1",
"postcss-normalize": "^7.0.1",
"postcss-preset-env": "^6.7.0",
"prettier": "^1.19.1",
"rollup": "^1.32.1",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-uglify": "^6.0.4",
"stylelint": "^13.6.1",
"stylelint-config-prettier": "^5.3.0",
"stylelint-config-standard": "^18.3.0",
"stylelint-prettier": "^1.1.2"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"build/*.js": [
"eslint --fix",
"prettier --write",
"git add"
],
"css/*.css": [
"stylelint --fix",
"prettier --write",
"git add"
],
"js/*.js": [
"eslint --fix",
"prettier --write",
"jest",
"git add"
]
},
"browserslist": [
"last 1 version",
"not dead",
"> 2% in GB"
],
"devDependencies": {
"@prettier/plugin-php": "^0.19.3",
"@types/jest": "^27.5.2",
"@types/jsdom": "^16.2.15",
"@typescript-eslint/eslint-plugin": "^5.15.0",
"@typescript-eslint/parser": "^5.15.0",
"eslint": "^8.11.0",
"eslint-config-prettier": "^8.5.0",
"jest": "^27.5.1",
"jsdom": "^19.0.0",
"prettier": "^2.8.3",
"ts-jest": "^27.1.3"
},
"dependencies": {
"copy-and-watch": "^0.1.6",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"cssnano": "^5.1.14",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.21",
"postcss-banner": "^4.0.1",
"postcss-cli": "^9.1.0",
"postcss-import": "^14.1.0",
"postcss-preset-env": "^7.8.3",
"rimraf": "^3.0.2",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-cli": "^4.10.0"
"jest": {
"testURL": "http://localhost:3000",
"collectCoverage": true,
"collectCoverageFrom": [
"js/**/*.js"
],
"roots": [
"js"
]
}
}

View File

@@ -1,6 +1,6 @@
const { resolve } = require("path");
const { resolve } = require('path');
const pkg = require(resolve(__dirname, "package.json"));
const pkg = require(resolve(__dirname, 'package.json'));
const year = new Date().getFullYear();
const banner = `Theme Name: WindyCOYS
@@ -13,15 +13,22 @@ Copyright: (c) ${year} ${pkg.author}
License: MIT
License URI: https://choosealicense.com/licenses/mit/`;
module.exports = (ctx) => ({
map: ctx.env !== "production",
module.exports = ctx => ({
map: ctx.env !== 'production',
plugins: {
"postcss-import": true,
"postcss-preset-env": {
"custom-media-queries": true,
'postcss-import': {},
'postcss-normalize': {},
'postcss-preset-env': {
stage: 0,
features: {
'custom-properties': {
preserve: false,
},
cssnano: ctx.env === "production" ? {} : false,
"postcss-banner": {
},
},
cssnano: ctx.env === 'production' ? {} : false,
'postcss-banner': {
banner,
important: true,
},

41
rollup.config.js Normal file
View File

@@ -0,0 +1,41 @@
const { resolve } = require('path');
const { uglify } = require('rollup-plugin-uglify');
const babel = require('rollup-plugin-babel');
const pkg = require('./package.json');
const year = new Date().getFullYear();
const production = process.env.NODE_ENV === 'production';
const banner = `/**!
* Theme Name: WindyCOYS
* Theme URI: https://github.com/danhead/windycoys/
* Author: ${pkg.author}
* Author URI: https://danielhead.com
* Description: Theme for windycoys.com
* Version: ${pkg.version}
* Copyright: (c) ${year} ${pkg.author}
* License: MIT
* License URI: https://choosealicense.com/licenses/mit/
*/`;
module.exports = {
input: resolve(__dirname, 'js/index.js'),
output: {
file: resolve(__dirname, 'theme/bundle.js'),
format: 'umd',
name: 'windy',
sourcemap: !production && 'inline',
banner,
},
plugins: [
babel({
exclude: 'node_modules/**',
}),
production && uglify({
output: {
preamble: banner,
},
}),
].filter(Boolean),
};

View File

@@ -1,18 +0,0 @@
<?php get_header(); ?>
<main class="page">
<section class="page__logo">
<?php get_template_part("partials/logo"); ?>
</section>
<section class="page__content">
<h1 class="title">404 - Page not found</h1>
<a class="link" href="<?php echo get_site_url(); ?>/">Go back home</a>
</section>
<section class="page__banner">
<?php get_template_part("partials/banner"); ?>
</section>
<section class="page__footer">
<?php get_template_part("partials/footer"); ?>
</section>
</main>
<?php get_template_part("partials/navigation"); ?>
<?php get_footer(); ?>

View File

@@ -1,36 +0,0 @@
<?php
class Windycoys_Walker_Navigation extends Walker_Nav_Menu
{
function start_lvl(&$output, $depth = 0, $args = [])
{
$output .= '<ul class="nav__list" role="menu">';
}
function end_lvl(&$output, $depth = 0, $args = [])
{
$output .= "</ul>";
}
function start_el(&$output, $item, $depth = 0, $args = [], $id = 0)
{
$title = $item->title;
$url = $item->url;
if ($title && $url) {
$output .= '<li class="nav__list-item" role="none">';
$output .= sprintf(
'<a class="nav__link" href="%s" role="menuitem">',
$url
);
$output .= $title;
$output .= "</a>";
}
}
function end_el(&$output, $item, $depth = 0, $args = [])
{
$output .= "</li>";
}
}
?>

View File

@@ -1,83 +0,0 @@
<?php
require get_template_directory() . "/classes/walker.comments.php";
$form_settings = [
"class_form" => "comment-respond__form",
"class_submit" => "comment-respond__submit",
"comment_field" =>
'<div class="comment-respond__item comment-respond__item--large"><label class="comment-respond__label" for="comment">Comment</label><textarea class="comment-respond__input" id="comment" name="comment" cols="25" rows="8" aria-required="true"></textarea></div>',
"logged_in_as" =>
'<p class="comment-respond__text">' .
sprintf(
__(
'Logged in as <a class="link" href="%1$s">%2$s</a>. <a class="link" href="%3$s" title="Log out of this account">Log out?</a>'
),
admin_url("profile.php"),
$user_identity,
wp_logout_url(apply_filters("the_permalink", get_permalink()))
) .
"</p>",
"title_reply_before" =>
'<h3 id="reply-title" class="comment-respond__title">',
"title_reply" => '<span class="comment-respond__title-text">Reply</span>',
"cancel_reply_before" => '<span class="comment-respond__cancel">',
"cancel_reply_after" => "</span>",
"comment_notes_before" =>
'<p class="comment-respond__notes">' .
__("Your email address will not be published.") .
($req ? $required_text : "") .
"</p>",
"fields" => [
"author" =>
'<div class="comment-respond__item"><label class="comment-respond__label" for="author">' .
__("Name", "domainreference") .
($req ? '<span class="comment-respond__required">*</span>' : "") .
"</label>" .
'<input class="comment-respond__input" id="author" name="author" type="text" value="' .
esc_attr($commenter["comment_author"]) .
'" size="60" aria-required="true" /></div>',
"email" =>
'<div class="comment-respond__item"><label class="comment-respond__label" for="email">' .
__("Email", "domainreference") .
($req ? '<span class="comment-respond__required">*</span>' : "") .
"</label>" .
'<input class="comment-respond__input" id="email" name="email" type="text" value="' .
esc_attr($commenter["comment_author_email"]) .
'" size="60" aria-required="true" /></div>',
"url" =>
'<div class="comment-respond__item"><label class="comment-respond__label" for="url">' .
__("Website", "domainreference") .
"</label>" .
'<input class="comment-respond__input" id="url" name="url" type="text" value="' .
esc_attr($commenter["comment_author_url"]) .
'" size="30" /></div>',
"cookies" =>
'<div class="comment-respond__item comment-respond__item--large"><input class="comment-respond__checkbox" id="wp-comment-cookies-consent" name="wp-comment-cookies-consent" type="checkbox" value="yes"/><label class="comment-respond__checkbox-label" for="wp-comment-cookies-consent">Save my name, email, and website in this browser for the next time I comment.</label></div>',
],
];
?>
<div class="comments" id="comments">
<div class="comments__head">
<h2 class="comments__title">
<?php if (comments_open()):
if (have_comments()):
echo "Join the conversation";
else:
echo "Leave a comment";
endif;
else:
echo "Comments are closed";
endif; ?>
</h2>
</div>
<div class="comments__body">
<ol class="comments__list">
<?php wp_list_comments([
"walker" => new Windycoys_Walker_Comments(),
]); ?>
</ol>
</div>
<div class="comments__foot">
<?php comment_form($form_settings); ?>
</div>
</div>

View File

View File

@@ -1,62 +0,0 @@
<?php get_header(); ?>
<main class="page">
<section class="page__logo is-fixed">
<?php get_template_part("partials/logo"); ?>
</section>
<section class="page__content page__content--centered-md">
<h1 class="title">Latest articles</h1>
<div class="page__columns">
<?php
$posts = get_posts([
"numberposts" => 4,
]);
foreach ($posts as $post) {
$date = strtotime($post->post_date);
$year = date("Y", $date);
$month = date("F", $date);
$day = date("jS", $date);
$date =
get_the_date(get_option("date_format"), $post->ID) .
" at " .
get_the_date(get_option("time_format"), $post->ID);
$content =
strlen($post->post_excerpt) === 0
? strip_tags(substr($post->post_content, 0, 200)) . " [...]"
: $post->post_excerpt;
$words = count(explode(" ", $post->post_content));
$excerpt = [
"title" => $post->post_title,
"url" => get_permalink($post->ID),
"content" => $content,
"author" => get_the_author_meta("nickname", $post->post_author),
"comments" => $post->comment_count,
"words" => $words,
"date" => $date,
];
echo '<div class="page__cell">';
get_template_part("partials/excerpt");
echo "</div>";
}
?>
</div>
<h2 class="title">Podcasts</h2>
<div class="page__columns">
<div class="page__cell">
<?php $podcast = windycoys_get_podcast_meta("xin"); ?>
<?php get_template_part("partials/podcast"); ?>
</div>
<div class="page__cell">
<?php $podcast = windycoys_get_podcast_meta("tfc"); ?>
<?php get_template_part("partials/podcast"); ?>
</div>
</div>
</section>
<section class="page__banner">
<?php get_template_part("partials/banner"); ?>
</section>
<section class="page__footer">
<?php get_template_part("partials/footer"); ?>
</section>
</main>
<?php get_template_part("partials/navigation"); ?>
<?php get_footer(); ?>

View File

@@ -1,234 +0,0 @@
<?php
function windycoys_the_tags()
{
$tags = get_the_tags();
if (!$tags) {
return false;
}
// Suppress NewsNow tag
foreach ($tags as $key => $tag) {
if ($tag->name === "newsnow") {
unset($tags[$key]);
}
}
if ($tags) {
foreach ($tags as $key => $tag) {
echo '<a class="metadata__tag-link" href="' .
home_url() .
"/tag/" .
$tag->name .
'" rel="tag">' .
$tag->name .
"</a>";
if ($key < count($tags) - 1) {
echo ", ";
}
}
}
}
function windycoys_get_twitter_share_url()
{
$url = "https://twitter.com/intent/tweet?text=";
$title = get_the_title();
$link = get_the_permalink();
return $url . urlencode($title . " : " . $link);
}
function windycoys_search_posts_per_page($query)
{
if ($query->is_search) {
$query->set("posts_per_page", "10");
}
return $query;
}
add_filter("pre_get_posts", "windycoys_search_posts_per_page");
function windycoys_get_deploy_timestamp()
{
if (file_exists(__DIR__ . "/.timestamp")) {
$timestamp = file_get_contents(__DIR__ . "/.timestamp");
return $timestamp;
}
return false;
}
function windycoys_create_podcasts_table()
{
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE windycoys_podcasts (
id int(2) NOT NULL AUTO_INCREMENT,
podcast_id varchar(3) DEFAULT '' NOT NULL,
last_update bigint(10) DEFAULT 0 NOT NULL,
content longtext DEFAULT '' NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";
require_once ABSPATH . "wp-admin/includes/upgrade.php";
dbDelta($sql);
$wpdb->insert("windycoys_podcasts", [
"podcast_id" => "tfc",
"last_update" => 0,
"content" => "{}",
]);
$wpdb->insert("windycoys_podcasts", [
"podcast_id" => "xin",
"last_update" => 0,
"content" => "{}",
]);
}
function windycoys_fetch_podcast_meta($id)
{
global $wpdb;
$urls = [
"tfc" => "https://rss.acast.com/thefightingcock",
"xin" => "https://rss.acast.com/theextrainch",
];
$feed = file_get_contents($urls[$id]);
$feed = str_replace("<media:", "<", $feed);
$feed = str_replace("</media:", "</", $feed);
$feed = str_replace("<itunes:", "<", $feed);
$feed = str_replace("</itunes:", "</", $feed);
$now = time();
$wpdb->update(
"windycoys_podcasts",
["last_update" => $now, "content" => $feed],
["podcast_id" => $id]
);
return $feed;
}
function windycoys_refresh_podcast_meta($id)
{
global $wpdb;
$now = time();
$res = $wpdb->get_row(
'SELECT * FROM windycoys_podcasts WHERE
podcast_id = "' .
$id .
'"'
);
if (!$res) {
windycoys_create_podcasts_table();
}
if ($res->last_update + 300 < $now) {
return [
"from_cache" => false,
"data" => windycoys_fetch_podcast_meta($id),
];
}
return [
"from_cache" => true,
"data" => $res->content,
];
}
function windycoys_get_episodes_url($id)
{
$urls = [
"tfc" => "https://play.acast.com/s/thefightingcock",
"xin" => "https://play.acast.com/s/theextrainch",
];
return $urls[$id];
}
function windycoys_get_podcast_meta($id)
{
$res = windycoys_refresh_podcast_meta($id);
$rss = simplexml_load_string($res["data"]);
$item = $rss->channel->item;
$date = strtotime((string) $item->pubDate);
return [
"id" => $id,
"from_cache" => $res["from_cache"],
"podcast_title" => (string) $rss->channel->title,
"podcast_description" => (string) $rss->channel->description,
"podcast_episodes" => windycoys_get_episodes_url($id),
"artwork_url" => (string) $rss->channel->image->url,
"title" => (string) $item->title,
"description" => (string) $item->description,
"image" => (string) $item->image["href"],
"date" =>
date(get_option("date_format"), $date) .
" at " .
date(get_option("time_format"), $date),
"length" => (string) $item->duration,
"url" => (string) $item->link,
"audio" => (string) $item->enclosure["url"],
];
}
function windycoys_get_read_time($words)
{
$wpm = 250;
$mins = ceil($words / $wpm);
return $mins;
}
function windycoys_is_ie()
{
$ua = htmlentities($_SERVER["HTTP_USER_AGENT"], ENT_QUOTES, "UTF-8");
if (
preg_match("~MSIE|Internet Explorer~i", $ua) ||
(strpos($ua, "Trident/7.0") !== false && strpos($ua, "rv:11.0") !== false)
) {
return true;
}
return false;
}
function windycoys_first_sentence($string)
{
$sentence = preg_split("/(\.|!|\?)\s/", $string, 2, PREG_SPLIT_DELIM_CAPTURE);
return $sentence["0"] . $sentence["1"];
}
function windycoys_get_description($post_id)
{
if (get_post_type() == "post") {
$post = get_post($post_id);
$content =
strlen($post->post_excerpt) === 0
? strip_tags(windycoys_first_sentence($post->post_content))
: $post->post_excerpt;
return $content;
} else {
return get_bloginfo("description");
}
}
function windycoys_jetpack_custom_image($media, $post_id, $args)
{
if ($media) {
return $media;
} else {
$url = get_template_directory_uri() . "/images/icon.png";
$permalink = get_permalink($post_id);
return [
[
"type" => "image",
"from" => "custom_fallback",
"src" => esc_url($url),
"href" => $permalink,
],
];
}
}
add_filter(
"jetpack_images_get_images",
"windycoys_jetpack_custom_image",
10,
3
);
function windycoys_strip_scripts(&$scripts)
{
if (!is_admin()) {
$scripts->remove("jquery");
}
}
add_filter("wp_default_scripts", "windycoys_strip_scripts");

View File

@@ -1 +0,0 @@
@import "./css/index";

View File

@@ -1,57 +0,0 @@
<?php get_header(); ?>
<main class="page">
<section class="page__logo">
<?php get_template_part("partials/logo"); ?>
</section>
<?php
$index = 0;
while (have_posts()):
the_post();
$index++;
?>
<section class="page__article<?php echo $index === 1 ? " is-first" : ""; ?>">
<article class="article">
<div class="article__date">
<svg class="icon icon--extra-small">
<use href="#icon-calendar"></use>
</svg>
<span class="article__date-text">
<?php the_time(get_option("date_format")); ?>
</span>
</div>
<div class="article__content">
<h1 class="title">
<a href="<?php the_permalink(); ?>">
<?php the_title(); ?>
</a>
</h1>
<div class="content">
<?php the_content(
__(
'Continue reading <span class="meta-nav">&raquo;</span>',
"windycoys"
)
); ?>
</div>
</div>
</article>
</section>
<section class="page__metadata">
<?php get_template_part("partials/metadata"); ?>
</section>
<?php
endwhile;
?>
<section class="page__pagination">
<?php get_template_part("partials/pagination"); ?>
</section>
<section class="page__banner">
<?php get_template_part("partials/banner"); ?>
</section>
<section class="page__footer">
<?php get_template_part("partials/footer"); ?>
</section>
</main>
<?php get_template_part("partials/navigation"); ?>
<?php get_footer(); ?>

View File

@@ -1,22 +0,0 @@
import { App } from "./ts";
document.addEventListener("DOMContentLoaded", () => new App());
/*
import { Image, Menu, Navigation, ReplyToComment } from "./ts";
document.addEventListener("DOMContentLoaded", () => {
Array.from(document.querySelectorAll("img")).forEach((image) => {
new Image(image).start();
});
const menu = document.querySelector("#menu") as HTMLButtonElement;
const navigation = document.querySelector(".nav") as HTMLElement;
const form = document.querySelector("#respond") as HTMLDivElement;
const links = Array.from(
document.querySelectorAll(".comment__reply")
) as HTMLLinkElement[];
const btn = new Menu(document.querySelector("#menu"), window);
btn.init();
new Navigation(navigation, btn).init();
new ReplyToComment(form, links).init();
});
*/

View File

@@ -1,64 +0,0 @@
<?php
$posts = get_posts([
"numberposts" => -1,
"order" => "ASC",
]);
$archive = [];
foreach ($posts as $post) {
$date = strtotime($post->post_date);
$year = date("Y", $date);
$month = date("F", $date);
$day = date("jS", $date);
$url = get_permalink($post->ID);
$comments =
$post->comment_count === "1"
? "1 comment"
: $post->comment_count . " comments";
if ($post->post_status === "publish") {
$archive[$year][$month][] = [
"title" => $post->post_title,
"slug" => $post->post_name,
"author" => $post->post_author,
"day" => $day,
"url" => $url,
"comments" => $comments,
];
}
}
?>
<div class="archive">
<ol class="archive__list">
<?php foreach ($archive as $year => $yearData): ?>
<li class="archive__item">
<h2 class="archive__heading">
<?php echo $year; ?>
</h2>
<ol class="archive__list archive__list--sub-list">
<?php foreach ($yearData as $month => $monthData): ?>
<li class="archive__item">
<h3 class="archive__subheading">
<?php echo $month; ?>
</h3>
<ol class="archive__list archive__list--sub-list">
<?php foreach ($monthData as $post): ?>
<li class="archive__item">
<span class="archive__date">
<?php echo $post["day"]; ?>
</span>
<a class="link" href="<?php echo $post["url"]; ?>">
<?php echo $post["title"]; ?>
</a>
<span class="archive__comments">
(<?php echo $post["comments"]; ?>)
</span>
</li>
<?php endforeach; ?>
</ol>
</li>
<?php endforeach; ?>
</ol>
</li>
<?php endforeach; ?>
</ol>
</div>

View File

@@ -1,14 +0,0 @@
<footer class="footer">
<div class="footer__head">
<p class="footer__text">&copy; <?php echo date("Y"); ?> <?php bloginfo(
"name"
); ?></p>
</div>
<div class="footer__body">
<a href="https://www.newsnow.co.uk/h/Sport/Football/Premier+League/Tottenham+Hotspur" title="Click here for more Tottenham Hotspur news from NewsNow">
<img src="<?php echo get_template_directory_uri(); ?>/images/newsnow_sw.gif" alt="As featured on NewsNow: Tottenham Hotspur news">
</a>
</div>
<?php get_template_part("icons"); ?>
</footer>
<?php wp_footer(); ?>

View File

@@ -1,84 +0,0 @@
<?php
global $wp;
$url = home_url($wp->request);
$base_url = substr($url, 0, strpos($url, "page/"));
$current = get_query_var("paged");
$total = $wp_query->max_num_pages;
$links = paginate_links([
"type" => "array",
"prev_next" => false,
"current" => $current,
"end_size" => 2,
]);
function get_previous_url($base_url, $page)
{
$qs = $_SERVER["QUERY_STRING"];
$url = $base_url . "page/" . ($page - 1);
if ($qs) {
$url .= "/?" . $qs;
}
return $url;
}
function get_next_url($base_url, $page)
{
$qs = $_SERVER["QUERY_STRING"];
$url = $base_url . "page/";
if ($page === 0) {
$url .= "2";
} else {
$url .= $page + 1;
}
if ($qs) {
$url .= "/?" . $qs;
}
return $url;
}
function format_link($link)
{
if (strpos($link, "page-numbers dots")) {
return '<span class="pagination__dots"><svg class="icon icon--extra-small"><use href="#icon-ellipsis"></use></svg></span>';
}
$link = str_replace("page-numbers current", "pagination__current", $link);
$link = str_replace("page-numbers", "link", $link);
return $link;
}
if (count($links) > 1): ?>
<div class="pagination">
<?php if ($current > 1): ?>
<div class="pagination__previous">
<a class="pagination__link" href="<?php echo get_previous_url(
$base_url,
$current
); ?>" aria-label="Previous page">
<svg class="icon icon--small">
<use href="#icon-chevron-left"></use>
</svg>
</a>
</div>
<?php endif; ?>
<nav class="pagination__content" role="navigation" aria-label="Pagination navigation">
<ul class="pagination__list">
<?php foreach ($links as $link) { ?>
<li class="pagination__item">
<?php echo format_link($link); ?>
</li>
<?php } ?>
</ul>
</nav>
<?php if ($current < $total): ?>
<div class="pagination__next">
<a class="pagination__link" href="<?php echo get_next_url(
$base_url,
$current
); ?>" aria-label="Next page">
<svg class="icon icon--small">
<use href="#icon-chevron-right"></use>
</svg>
</a>
</div>
<?php endif; ?>
</div>
<?php endif; ?>

View File

@@ -1,62 +0,0 @@
<?php
global $wp_query;
get_header();
$query = $_GET["s"];
$results = $wp_query->found_posts;
?>
<main class="page">
<section class="page__logo is-fixed">
<?php get_template_part("partials/logo"); ?>
</section>
<section class="page__content">
<?php if ($results === 0): ?>
<h1 class="title">Sorry, no results found.</h1>
<a class="link" href="<?php echo get_site_url(); ?>/">Go back home</a>
<?php else: ?>
<h1 class="title">
<?php echo $results .
" result" .
($results !== "1" ? "s" : "") .
' matching "' .
$query .
'" found.'; ?>
</h1>
<?php endif; ?>
<ol>
<?php if (have_posts()):
while (have_posts()):
the_post(); ?>
<li>
<?php
$excerpt = [
"title" => get_the_title(),
"url" => get_the_permalink(),
"content" => get_the_excerpt(),
"author" => get_the_author_meta("nickname"),
"comments" => get_comments_number(),
"date" =>
get_the_date(get_option("date_format"), $id) .
" at " .
get_the_date(get_option("time_format"), $id),
"words" => count(explode(" ", $post->post_content)),
];
get_template_part("partials/excerpt");
?>
</li>
<?php
endwhile;
endif; ?>
</ol>
</section>
<section class="page__pagination">
<?php get_template_part("partials/pagination"); ?>
</section>
<section class="page__banner">
<?php get_template_part("partials/banner"); ?>
</section>
<section class="page__footer">
<?php get_template_part("partials/footer"); ?>
</section>
</main>
<?php get_template_part("partials/navigation"); ?>
<?php get_footer(); ?>

View File

@@ -1,62 +0,0 @@
<?php get_header(); ?>
<main class="page">
<section class="page__logo is-fixed">
<?php get_template_part("partials/logo"); ?>
</section>
<section class="page__content">
<h1 class="title">
<span class="title__icon">
<svg class="icon" aria-hidden="true">
<use href="#icon-tag"></use>
</svg>
</span>
<span class="title__text">
<?php echo single_tag_title(); ?>
</span>
</h1>
<ol>
<?php if (have_posts()):
while (have_posts()):
the_post(); ?>
<li>
<?php
$excerpt = [
"title" => get_the_title(),
"url" => get_the_permalink(),
"content" => get_the_excerpt(),
"author" => get_the_author_meta("nickname"),
"comments" => get_comments_number(),
"date" =>
get_the_date(get_option("date_format"), $id) .
" at " .
get_the_date(get_option("time_format"), $id),
"words" => count(explode(" ", $post->post_content)),
];
get_template_part("partials/excerpt");
?>
</li>
<?php
endwhile; ?>
</ol>
</section>
<section class="page__pagination">
<?php get_template_part("partials/pagination"); ?>
</section>
<?php
else:
?>
<section class="page__content">
<h1 class="title">Sorry, no results found.</h1>
<a class="link" href="<?php echo get_site_url(); ?>/">Go back home</a>
</section>
<?php
endif; ?>
<section class="page__banner">
<?php get_template_part("partials/banner"); ?>
</section>
<section class="page__footer">
<?php get_template_part("partials/footer"); ?>
</section>
</main>
<?php get_template_part("partials/navigation"); ?>
<?php get_footer(); ?>

View File

@@ -1,75 +0,0 @@
import { Image } from "./image";
import { Menu } from "./menu";
import { Navigation } from "./navigation";
import { ReplyToComment } from "./reply-to-comment";
export class App {
menu: Menu;
navigation: Navigation;
constructor() {
Array.from(document.querySelectorAll("img")).forEach((image) =>
new Image(image).start()
);
this.menu = new Menu(document.querySelector("#menu"), window);
this.navigation = new Navigation(document.querySelector(".nav"));
new ReplyToComment();
this.attachEvents();
this.attachKeyEvents();
}
attachEvents(): void {
const { menu, navigation } = this;
menu.btn.addEventListener("click", (e) => {
e.preventDefault();
this.showNav();
});
navigation.container.addEventListener("click", (e) => {
if (navigation.container === e.target) {
this.hideNav();
}
});
navigation.container
.querySelector(".nav__close")
.addEventListener("click", () => {
this.hideNav();
});
}
attachKeyEvents() {
const { navigation } = this;
const first = navigation.container.querySelector(
".nav__link"
) as HTMLElement;
const last = navigation.container.querySelector(
".nav__close"
) as HTMLElement;
navigation.container.addEventListener("keydown", (e) => {
if (e.code === "Escape") {
this.hideNav();
}
});
first.addEventListener("keydown", (e) => {
if (e.shiftKey && e.code === "Tab") {
e.preventDefault();
last.focus();
}
});
last.addEventListener("keydown", (e) => {
if (!e.shiftKey && e.code === "Tab") {
e.preventDefault();
first.focus();
}
});
}
showNav(): void {
this.menu.hide();
this.navigation.show();
}
hideNav(): void {
this.menu.show();
this.navigation.hide();
}
}

View File

@@ -1,27 +0,0 @@
export class Image {
image: HTMLImageElement;
constructor(img: HTMLImageElement) {
this.image = img;
}
fadeIn(): void {
this.image.style.opacity = "1";
}
attachEvents(): void {
this.image.addEventListener("load", () => this.fadeIn());
}
start(): void {
if (this.image.dataset.loadTransition === "false") {
return;
}
this.image.style.opacity = "0";
if (this.image.complete) {
this.fadeIn();
} else {
this.attachEvents();
}
}
}

View File

@@ -1 +0,0 @@
export * from "./app";

View File

@@ -1,55 +0,0 @@
export class Menu {
window: Window;
btn: HTMLButtonElement;
state: "hidden" | "minimized" | "visible";
lastScrollPosition: number;
constructor(button: HTMLButtonElement, window: Window) {
this.btn = button;
this.window = window;
this.state = "visible";
this.lastScrollPosition = 0;
this.btn.setAttribute("aria-expanded", "false");
this.attachEvents();
setTimeout(() => {
this.show();
}, 1000);
}
attachEvents(): void {
this.window.addEventListener("scroll", () => {
this.updateState(this.lastScrollPosition, window.scrollY);
this.lastScrollPosition = window.scrollY;
});
}
updateState(lastY = 0, currentY: number): void {
if (this.state !== "hidden") {
if (this.state !== "visible" && currentY < lastY) {
this.show();
} else if (this.state !== "minimized" && currentY > lastY) {
this.minimize();
}
}
}
show(): void {
this.state = "visible";
this.btn.setAttribute("aria-expanded", "false");
this.btn.classList.remove("is-minimized");
this.btn.classList.add("is-visible");
}
minimize(): void {
this.state = "minimized";
this.btn.classList.remove("is-visible");
this.btn.classList.add("is-minimized");
}
hide(): void {
this.state = "hidden";
this.btn.setAttribute("aria-expanded", "true");
this.btn.classList.remove("is-visible");
this.btn.classList.remove("is-minimized");
}
}

View File

@@ -1,26 +0,0 @@
export class Navigation {
container: HTMLElement;
title: HTMLElement;
lastActiveElement: HTMLElement;
constructor(container: HTMLElement) {
this.container = container;
this.title = container.querySelector(".nav__title-link") as HTMLElement;
this.container.setAttribute("aria-hidden", "true");
}
show() {
this.lastActiveElement = document.activeElement as HTMLElement;
this.container.classList.add("is-open");
this.container.setAttribute("aria-hidden", "false");
this.title.focus();
}
hide() {
this.container.classList.remove("is-open");
this.container.setAttribute("aria-hidden", "true");
if (this.lastActiveElement) {
this.lastActiveElement.focus();
}
}
}

View File

@@ -1,72 +0,0 @@
export class ReplyToComment {
clonedForm: HTMLDivElement;
form: HTMLDivElement;
links: HTMLLinkElement[];
constructor() {
this.form = document.querySelector("#respond") as HTMLDivElement;
if (this.form) {
this.links = Array.from(
document.querySelectorAll(".comment__reply")
) as HTMLLinkElement[];
this.clonedForm = this.form.cloneNode(true) as HTMLDivElement;
if (this.links.length > 0) {
this.attachEvents();
}
}
}
attachEvents(): void {
this.links.forEach((link) => {
link.addEventListener("click", (e) => this.handleLinkClick(e, link));
});
}
handleLinkClick(e: Event, link: HTMLLinkElement): void {
e.preventDefault();
link.classList.add("is-hidden");
this.form.style.display = "none";
const targetNode = link.parentNode.querySelector(
".comment__inline-reply"
) as HTMLDivElement;
const { commentId, commentAuthor } = link.dataset;
this.insertForm(targetNode, commentId, commentAuthor);
}
insertForm(
targetNode: HTMLDivElement,
commentId: string,
commentAuthor: string
): void {
const clonedForm = this.clonedForm.cloneNode(true) as HTMLDivElement;
const cancel = clonedForm.querySelector(
"#cancel-comment-reply-link"
) as HTMLLinkElement;
const submit = clonedForm.querySelector("#submit") as HTMLInputElement;
const commentParent = clonedForm.querySelector(
"#comment_parent"
) as HTMLInputElement;
clonedForm.querySelector(
".comment-respond__title-text"
).innerHTML = `Reply to ${commentAuthor}`;
submit.value = "Post reply";
commentParent.value = commentId;
targetNode.appendChild(clonedForm);
cancel.style.display = "inline-block";
cancel.addEventListener("click", (e) =>
this.handleCancelClick(e, targetNode)
);
}
handleCancelClick(e: Event, container: HTMLDivElement) {
e.preventDefault();
container.parentNode
.querySelector(".comment__reply")
.classList.remove("is-hidden");
while (container.firstChild) {
container.removeChild(container.firstChild);
}
this.form.style.display = "block";
}
}

18
theme/404.php Normal file
View File

@@ -0,0 +1,18 @@
<?php get_header(); ?>
<main class="page">
<section class="page__logo">
<?php get_template_part('partials/logo') ?>
</section>
<section class="page__content">
<h1 class="title">404 - Page not found</h1>
<a class="link" href="<?php echo get_site_url() ?>/">Go back home</a>
</section>
<section class="page__banner">
<?php get_template_part('partials/banner') ?>
</section>
<section class="page__footer">
<?php get_template_part('partials/footer') ?>
</section>
</main>
<?php get_template_part('partials/navigation') ?>
<?php get_footer() ?>

View File

@@ -1,61 +1,48 @@
<?php
class Windycoys_Walker_Comments extends Walker_Comment
{
function start_lvl(&$output, $depth = 0, $args = [])
{
class Windycoys_Walker_Comments extends Walker_Comment {
function start_lvl(&$output, $depth = 0, $args = Array()) {
$output .= '<ol class="comments__list comments__list--sub-list">';
}
function end_lvl(&$output, $depth = 0, $args = [])
{
$output .= "</ol>";
function end_lvl(&$output, $depth = 0, $args = Array()) {
$output .= '</ol>';
}
function start_el(&$output, $comment, $depth = 0, $args = [], $id = 0)
{
function start_el(&$output, $comment, $depth = 0 , $args = Array(), $id = 0) {
$id = $comment->comment_ID;
$post_id = $comment->comment_post_ID;
$reply_link = get_permalink($post_id) . "?replytocom=" . $id . "#respond";
$reply_link = get_permalink($post_id) . '?replytocom='.$id.'#respond';
$author_email = $comment->comment_author_email;
$author = $comment->comment_author;
$avatar = get_avatar($comment, 128);
$date =
get_comment_date(get_option("date_format"), $id) .
" at " .
get_comment_date(get_option("time_format"), $id);
$date = get_comment_date(get_option('date_format'), $id).' at '.get_comment_date(get_option('time_format'), $id);
$content = $comment->comment_content;
$output .= '<li class="comments__item">';
$output .= '<div class="comment">';
$output .= '<div class="comment__head">';
$output .= sprintf(
'<div class="comment__avatar has-silhouette">%s</div>',
$avatar
);
$output .= sprintf('<div class="comment__avatar has-silhouette">%s</div>', $avatar);
$output .= '<div class="comment__metadata">';
$output .= sprintf('<span class="comment__author">%s</span>', $author);
$output .= sprintf('<span class="comment__date">%s</span>', $date);
$output .= "</div></div>";
$output .= '</div></div>';
$output .= sprintf('<div class="comment__body">%s</div>', $content);
$output .= '<div class="comment__foot">';
$output .= sprintf(
'<a class="comment__reply" data-comment-author="%s" ' .
$output .= sprintf('<a class="comment__reply" data-comment-author="%s" ' .
'data-comment-id="%s" href="%s">',
$author,
$id,
$reply_link
);
$output .= '<span class="comment__reply-icon">';
$output .=
'<svg class="icon icon--extra-small"><use href="#icon-reply"></use></svg></span>';
$output .= '<svg class="icon icon--extra-small"><use xlink:href="#icon-reply"></use></svg></span>';
$output .= '<span class="comment__reply-text">Reply</span></a>';
$output .= '<div class="comment__inline-reply"></div>';
$output .= "</div></div>";
$output .= '</div></div>';
}
function end_el(&$output, $comment, $depth = 0, $args = [])
{
$output .= "</li>";
function end_el(&$output, $comment, $depth = 0, $args = Array()) {
$output .= '</li>';
}
}

View File

@@ -0,0 +1,28 @@
<?php
class Windycoys_Walker_Navigation extends Walker_Nav_Menu {
function start_lvl(&$output, $depth = 0, $args = Array()) {
$output .= '<ul class="nav__list" role="menu">';
}
function end_lvl(&$output, $depth = 0, $args = Array()) {
$output .= '</ul>';
}
function start_el(&$output, $item, $depth = 0, $args = Array(), $id = 0) {
$title = $item->title;
$url = $item->url;
if ($title && $url) {
$output .= '<li class="nav__list-item" role="none">';
$output .= sprintf('<a class="nav__link" href="%s" role="menuitem">', $url);
$output .= $title;
$output .= '</a>';
}
}
function end_el(&$output, $item, $depth = 0, $args = Array()) {
$output .= '</li>';
}
}
?>

50
theme/comments.php Normal file
View File

@@ -0,0 +1,50 @@
<?php
require get_template_directory() . '/classes/walker.comments.php';
$form_settings = array(
class_form => 'comment-respond__form',
class_submit => 'comment-respond__submit',
comment_field => '<div class="comment-respond__item comment-respond__item--large"><label class="comment-respond__label" for="comment">Comment</label><textarea class="comment-respond__input" id="comment" name="comment" cols="25" rows="8" aria-required="true"></textarea></div>',
logged_in_as => '<p class="comment-respond__text">' . sprintf( __( 'Logged in as <a class="link" href="%1$s">%2$s</a>. <a class="link" href="%3$s" title="Log out of this account">Log out?</a>' ), admin_url( 'profile.php' ), $user_identity, wp_logout_url( apply_filters( 'the_permalink', get_permalink( ) ) ) ) . '</p>',
title_reply_before => '<h3 id="reply-title" class="comment-respond__title">',
title_reply => '<span class="comment-respond__title-text">Reply</span>',
cancel_reply_before => '<span class="comment-respond__cancel">',
cancel_reply_after => '</span>',
comment_notes_before => '<p class="comment-respond__notes">' . __( 'Your email address will not be published.' ) . ( $req ? $required_text : '' ) . '</p>',
fields => array(
author => '<div class="comment-respond__item"><label class="comment-respond__label" for="author">' . __( 'Name', 'domainreference' ) . ( $req ? '<span class="comment-respond__required">*</span>' : '' ) . '</label>' . '<input class="comment-respond__input" id="author" name="author" type="text" value="' . esc_attr( $commenter['comment_author'] ) . '" size="30"' . $aria_req . ' /></div>',
email => '<div class="comment-respond__item"><label class="comment-respond__label" for="email">' . __( 'Email', 'domainreference' ) . ( $req ? '<span class="comment-respond__required">*</span>' : '' ) . '</label>' . '<input class="comment-respond__input" id="email" name="email" type="text" value="' . esc_attr( $commenter['comment_author_email'] ) . '" size="30"' . $aria_req . ' /></div>',
url => '<div class="comment-respond__item"><label class="comment-respond__label" for="url">' . __( 'Website', 'domainreference' ) . '</label>' . '<input class="comment-respond__input" id="url" name="url" type="text" value="' . esc_attr( $commenter['comment_author_url'] ) . '" size="30" /></div>',
cookies => '<div class="comment-respond__item comment-respond__item--large"><input class="comment-respond__checkbox" id="wp-comment-cookies-consent" name="wp-comment-cookies-consent" type="checkbox" value="yes"/><label class="comment-respond__checkbox-label" for="wp-comment-cookies-consent">Save my name, email, and website in this browser for the next time I comment.</label></div>',
),
);
?>
<div class="comments" id="comments">
<div class="comments__head">
<h2 class="comments__title">
<?php
if (comments_open()) :
if (have_comments()) :
echo 'Join the conversation';
else:
echo 'Leave a comment';
endif;
else:
echo 'Comments are closed';
endif;
?>
</h2>
</div>
<div class="comments__body">
<ol class="comments__list">
<?php
wp_list_comments(array(
walker => new Windycoys_Walker_Comments(),
));
?>
</ol>
</div>
<div class="comments__foot">
<?php comment_form($form_settings) ?>
</div>
</div>

View File

@@ -1,12 +1,12 @@
<?php
$js_url = get_template_directory_uri() . "/bundle.js";
$js_url = get_template_directory_uri() . '/bundle.js';
$deploy_timestamp = windycoys_get_deploy_timestamp();
if ($deploy_timestamp) {
$js_url .= "?t=" . $deploy_timestamp;
$js_url .= '?t=' . $deploy_timestamp;
}
?>
<div id="fb-root"></div>
<script type="text/javascript" src="<?php echo $js_url; ?>"></script>
<script type="text/javascript" src="<?php echo $js_url ?>"></script>
<script type="text/javascript">
(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];

58
theme/front-page.php Normal file
View File

@@ -0,0 +1,58 @@
<?php get_header() ?>
<main class="page">
<section class="page__logo is-fixed">
<?php get_template_part('partials/logo') ?>
</section>
<section class="page__content page__content--centered-md">
<h1 class="title">Latest articles</h1>
<div class="page__columns">
<?php
$posts = get_posts(array(
numberposts => 4,
));
foreach ($posts as $post) {
$date = strtotime($post->post_date);
$year = date('Y', $date);
$month = date('F', $date);
$day = date('jS', $date);
$date = get_the_date(get_option('date_format'), $post->ID).' at '.get_the_date(get_option('time_format'), $post->ID);
$content = (strlen($post->post_excerpt) === 0) ?
strip_tags(substr($post->post_content, 0, 200)).' [...]' :
$post->post_excerpt;
$words = count(explode(" ", $post->post_content));
$excerpt = array(
title => $post->post_title,
url => get_permalink($post->ID),
content => $content,
author => get_the_author_meta('nickname', $post->post_author),
comments => $post->comment_count,
words => $words,
date => $date,
);
echo '<div class="page__cell">';
get_template_part('partials/excerpt');
echo '</div>';
}
?>
</div>
<h2 class="title">Podcasts</h2>
<div class="page__columns">
<div class="page__cell">
<?php $podcast = windycoys_get_podcast_meta('xin') ?>
<?php get_template_part('partials/podcast') ?>
</div>
<div class="page__cell">
<?php $podcast = windycoys_get_podcast_meta('tfc') ?>
<?php get_template_part('partials/podcast') ?>
</div>
</div>
</section>
<section class="page__banner">
<?php get_template_part('partials/banner') ?>
</section>
<section class="page__footer">
<?php get_template_part('partials/footer') ?>
</section>
</main>
<?php get_template_part('partials/navigation') ?>
<?php get_footer() ?>

196
theme/functions.php Normal file
View File

@@ -0,0 +1,196 @@
<?php
function windycoys_the_tags() {
$tags = get_the_tags();
if (!$tags) return false;
// Suppress NewsNow tag
foreach($tags as $key=>$tag) {
if($tag->name === "newsnow") {
unset($tags[$key]);
}
}
if ($tags) {
foreach($tags as $key=>$tag) {
echo '<a class="metadata__tag-link" href="' .
home_url() . '/tag/' . $tag->name . '" rel="tag">' .
$tag->name . '</a>';
if ($key < count($tags) - 1) {
echo ', ';
}
}
}
}
function windycoys_get_twitter_share_url() {
$url = 'https://twitter.com/intent/tweet?text=';
$title = get_the_title();
$link = get_the_permalink();
return $url . urlencode($title . ' : ' . $link);
}
function windycoys_search_posts_per_page($query) {
if ($query->is_search) {
$query->set( 'posts_per_page', '10' );
}
return $query;
}
add_filter('pre_get_posts', 'windycoys_search_posts_per_page');
function windycoys_get_deploy_timestamp() {
if (file_exists(__DIR__ . '/.timestamp')) {
$timestamp = file_get_contents(__DIR__ . '/.timestamp');
return $timestamp;
}
return false;
}
function windycoys_create_podcasts_table() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE windycoys_podcasts (
id int(2) NOT NULL AUTO_INCREMENT,
podcast_id varchar(3) DEFAULT '' NOT NULL,
last_update bigint(10) DEFAULT 0 NOT NULL,
content longtext DEFAULT '' NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
$wpdb->insert('windycoys_podcasts', array(
'podcast_id' => 'tfc',
'last_update' => 0,
'content' => '{}'
));
$wpdb->insert('windycoys_podcasts', array(
'podcast_id' => 'xin',
'last_update' => 0,
'content' => '{}'
));
}
function windycoys_fetch_podcast_meta($id) {
global $wpdb;
$urls = array(
'tfc' => 'https://rss.acast.com/thefightingcock',
'xin' => 'https://rss.acast.com/theextrainch',
);
$feed = file_get_contents($urls[$id]);
$feed = str_replace('<media:', '<', $feed);
$feed = str_replace('</media:', '</', $feed);
$feed = str_replace('<itunes:', '<', $feed);
$feed = str_replace('</itunes:', '</', $feed);
$now = time();
$wpdb->update("windycoys_podcasts",
array('last_update' => $now, 'content' => $feed),
array('podcast_id'=>$id)
);
return $feed;
}
function windycoys_refresh_podcast_meta($id) {
global $wpdb;
$now = time();
$res = $wpdb->get_row('SELECT * FROM windycoys_podcasts WHERE
podcast_id = "' . $id .'"');
if (!$res) {
windycoys_create_podcasts_table();
}
if ($res->last_update + 300 < $now) {
return array(
from_cache => false,
data => windycoys_fetch_podcast_meta($id),
);
}
return array(
from_cache => true,
data => $res->content,
);
}
function windycoys_get_episodes_url($id) {
$urls = array(
'tfc' => 'https://play.acast.com/s/thefightingcock',
'xin' => 'https://play.acast.com/s/theextrainch',
);
return $urls[$id];
}
function windycoys_get_podcast_meta($id) {
$res = windycoys_refresh_podcast_meta($id);
$rss = simplexml_load_string($res['data']);
$item = $rss->channel->item;
$date = strtotime((String) $item->pubDate);
return array(
id => $id,
from_cache => $res['from_cache'],
podcast_title => (String) $rss->channel->title,
podcast_description => (String) $rss->channel->description,
podcast_episodes => windycoys_get_episodes_url($id),
artwork_url => (String) $rss->channel->image->url,
title => (String) $item->title,
description => (String) $item->description,
image => (String) $item->image['href'],
date => date(get_option('date_format'), $date).' at '.date(get_option('time_format'), $date),
length => (String) $item->duration,
url => (String) $item->link,
audio => (String) $item->enclosure['url'],
);
}
function windycoys_get_read_time($words) {
$wpm = 250;
$mins = ceil($words / $wpm);
return $mins;
}
function windycoys_is_ie() {
$ua = htmlentities($_SERVER["HTTP_USER_AGENT"], ENT_QUOTES, "UTF-8");
if (preg_match("~MSIE|Internet Explorer~i", $ua) || (strpos($ua, "Trident/7.0") !== false && strpos($ua, "rv:11.0") !== false)) {
return true;
}
return false;
}
function windycoys_first_sentence($string) {
$sentence = preg_split( '/(\.|!|\?)\s/', $string, 2, PREG_SPLIT_DELIM_CAPTURE );
return $sentence['0'] . $sentence['1'];
}
function windycoys_get_description($post_id) {
if (get_post_type() == "post") {
$post = get_post($post_id);
$content = (strlen($post->post_excerpt) === 0) ?
strip_tags(windycoys_first_sentence($post->post_content)) :
$post->post_excerpt;
return $content;
} else {
return get_bloginfo('description');
}
}
function windycoys_jetpack_custom_image($media, $post_id, $args) {
if ($media) {
return $media;
} else {
$url = get_template_directory_uri() . '/images/icon.png';
$permalink = get_permalink($post_id);
return array(
array(
'type' => 'image',
'from' => 'custom_fallback',
'src' => esc_url($url),
'href' => $permalink,
)
);
}
}
add_filter('jetpack_images_get_images', 'windycoys_jetpack_custom_image', 10, 3);
function windycoys_strip_scripts(&$scripts) {
if(!is_admin()){
$scripts->remove('jquery');
}
}
add_filter('wp_default_scripts', 'windycoys_strip_scripts');

Some files were not shown because too many files have changed in this diff Show More