refactor
33
.babelrc
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
10
.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier",
|
||||
],
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"plugins": [
|
||||
"prettier"
|
||||
],
|
||||
"extends": [
|
||||
"airbnb-base",
|
||||
"prettier"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"jest": true
|
||||
},
|
||||
"rules": {
|
||||
"prettier/prettier": "error"
|
||||
}
|
||||
}
|
||||
46
.gitignore
vendored
@@ -1,43 +1,5 @@
|
||||
# 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
|
||||
.tmp
|
||||
.old
|
||||
.env
|
||||
|
||||
# Distribution assets
|
||||
theme/images
|
||||
theme/bundle.js
|
||||
theme/style.css
|
||||
theme/.timestamp
|
||||
dist
|
||||
node_modules
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"trailingComma": "all"
|
||||
}
|
||||
|
||||
13
.stylelintrc
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"plugins": [
|
||||
"stylelint-prettier"
|
||||
],
|
||||
"extends": [
|
||||
"stylelint-config-standard",
|
||||
"stylelint-prettier/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"prettier/prettier": true,
|
||||
"selector-pseudo-element-colon-notation": "single"
|
||||
}
|
||||
}
|
||||
91
README.md
@@ -1,91 +0,0 @@
|
||||
<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)
|
||||
1
browser-sync/.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
6
browser-sync/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM node:18
|
||||
WORKDIR /usr/src/app
|
||||
COPY . .
|
||||
RUN npm ci
|
||||
EXPOSE 3000
|
||||
CMD ["node", "index.js"]
|
||||
20
browser-sync/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
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",
|
||||
});
|
||||
2970
browser-sync/package-lock.json
generated
Normal file
11
browser-sync/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
6
build/deploy.sh
Executable file → Normal file
@@ -8,6 +8,6 @@ hash rsync 2>/dev/null || { echo >&2 "Error: rsync is not installed. Aborting.";
|
||||
[ -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 with rsync
|
||||
rsync -e "ssh -o StrictHostKeyChecking=no" -avzuh --progress \
|
||||
theme/. $SSH_USER@$SSH_IP:~/windycoys_theme_$1
|
||||
# Deploy theme
|
||||
scp ./build/docker-compose.yml $SSH_USER@$SSH_IP:~/windycoys/
|
||||
scp -r ./dist $SSH_USER@$SSH_IP:~/windycoys/
|
||||
|
||||
36
build/docker-compose.dev.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
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"
|
||||
@@ -1,22 +1,28 @@
|
||||
wordpress:
|
||||
environment:
|
||||
- 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
|
||||
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_
|
||||
volumes:
|
||||
- ./.tmp/wp-content:/var/www/html/wp-content
|
||||
- ./dist:/var/www/html/wp-content/themes/windycoys
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Write deploy timestamp to disk
|
||||
timestamp=$(date +%s)
|
||||
echo $timestamp > theme/.timestamp
|
||||
@@ -1,34 +0,0 @@
|
||||
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
Executable file → Normal file
@@ -12,4 +12,3 @@ curl -X DELETE \
|
||||
-H "X-Auth-Email: $CLOUDFLARE_EMAIL" \
|
||||
--data '{"purge_everything":true}' \
|
||||
"https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_IDENTIFIER/purge_cache"
|
||||
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
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);
|
||||
},
|
||||
);
|
||||
2
css/normalize.css
vendored
@@ -1,2 +0,0 @@
|
||||
/* stylelint-disable at-rule-no-unknown */
|
||||
@import-normalize;
|
||||
7
jest.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
roots: ["src/ts"],
|
||||
};
|
||||
|
||||
26
js/image.js
@@ -1,26 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
import windy from "./windy";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", windy);
|
||||
59
js/menu.js
@@ -1,59 +0,0 @@
|
||||
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
@@ -1,114 +0,0 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -1,78 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
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
@@ -1,31 +0,0 @@
|
||||
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"),
|
||||
});
|
||||
}
|
||||
29948
package-lock.json
generated
131
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windycoys",
|
||||
"version": "1.0.2",
|
||||
"version": "2.0.0-alpha1",
|
||||
"description": "Wordpress theme for windycoys.com",
|
||||
"author": "Dan Head <me@danielhead.com>",
|
||||
"license": "MIT",
|
||||
@@ -9,98 +9,55 @@
|
||||
"url": "https://github.com/danhead/windycoys"
|
||||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"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",
|
||||
"postinstall": "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",
|
||||
"watch": "run-p watch:*",
|
||||
"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"
|
||||
]
|
||||
"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/"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 1 version",
|
||||
"not dead",
|
||||
"> 2% in GB"
|
||||
],
|
||||
"jest": {
|
||||
"testURL": "http://localhost:3000",
|
||||
"collectCoverage": true,
|
||||
"collectCoverageFrom": [
|
||||
"js/**/*.js"
|
||||
],
|
||||
"roots": [
|
||||
"js"
|
||||
]
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,22 +13,15 @@ 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': {},
|
||||
'postcss-normalize': {},
|
||||
'postcss-preset-env': {
|
||||
stage: 0,
|
||||
features: {
|
||||
'custom-properties': {
|
||||
preserve: false,
|
||||
},
|
||||
},
|
||||
"postcss-import": true,
|
||||
"postcss-preset-env": {
|
||||
"custom-media-queries": true,
|
||||
},
|
||||
cssnano: ctx.env === 'production' ? {} : false,
|
||||
'postcss-banner': {
|
||||
cssnano: ctx.env === "production" ? {} : false,
|
||||
"postcss-banner": {
|
||||
banner,
|
||||
important: true,
|
||||
},
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
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),
|
||||
};
|
||||
@@ -35,7 +35,7 @@ class Windycoys_Walker_Comments extends Walker_Comment {
|
||||
$reply_link
|
||||
);
|
||||
$output .= '<span class="comment__reply-icon">';
|
||||
$output .= '<svg class="icon icon--extra-small"><use xlink:href="#icon-reply"></use></svg></span>';
|
||||
$output .= '<svg class="icon icon--extra-small"><use 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>';
|
||||
50
src/comments.php
Normal 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="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(array(
|
||||
"walker" => new Windycoys_Walker_Comments(),
|
||||
));
|
||||
?>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="comments__foot">
|
||||
<?php comment_form($form_settings) ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,38 +1,27 @@
|
||||
@import "normalize";
|
||||
@import "base";
|
||||
@import "page";
|
||||
@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 "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 "social";
|
||||
@import "title";
|
||||
|
||||
: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;
|
||||
body {
|
||||
--space-xxs: 0.25rem;
|
||||
--space-xs: 0.5rem;
|
||||
--space-sm: 1rem;
|
||||
@@ -66,6 +55,19 @@
|
||||
--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;
|
||||
0
src/css/theme.css
Normal file
@@ -8,7 +8,7 @@
|
||||
<div class="page__columns">
|
||||
<?php
|
||||
$posts = get_posts(array(
|
||||
numberposts => 4,
|
||||
"numberposts" => 4,
|
||||
));
|
||||
foreach ($posts as $post) {
|
||||
$date = strtotime($post->post_date);
|
||||
@@ -21,13 +21,13 @@
|
||||
$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,
|
||||
"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');
|
||||
@@ -71,8 +71,8 @@ function windycoys_create_podcasts_table() {
|
||||
function windycoys_fetch_podcast_meta($id) {
|
||||
global $wpdb;
|
||||
$urls = array(
|
||||
'tfc' => 'https://rss.acast.com/thefightingcock',
|
||||
'xin' => 'https://rss.acast.com/theextrainch',
|
||||
"tfc" => 'https://rss.acast.com/thefightingcock',
|
||||
"xin" => 'https://rss.acast.com/theextrainch',
|
||||
);
|
||||
$feed = file_get_contents($urls[$id]);
|
||||
$feed = str_replace('<media:', '<', $feed);
|
||||
@@ -97,20 +97,20 @@ function windycoys_refresh_podcast_meta($id) {
|
||||
}
|
||||
if ($res->last_update + 300 < $now) {
|
||||
return array(
|
||||
from_cache => false,
|
||||
data => windycoys_fetch_podcast_meta($id),
|
||||
"from_cache" => false,
|
||||
"data" => windycoys_fetch_podcast_meta($id),
|
||||
);
|
||||
}
|
||||
return array(
|
||||
from_cache => true,
|
||||
data => $res->content,
|
||||
"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',
|
||||
"tfc" => 'https://play.acast.com/s/thefightingcock',
|
||||
"xin" => 'https://play.acast.com/s/theextrainch',
|
||||
);
|
||||
return $urls[$id];
|
||||
}
|
||||
@@ -121,19 +121,19 @@ function windycoys_get_podcast_meta($id) {
|
||||
$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'],
|
||||
"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'],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -177,10 +177,10 @@ function windycoys_jetpack_custom_image($media, $post_id, $args) {
|
||||
|
||||
return array(
|
||||
array(
|
||||
'type' => 'image',
|
||||
'from' => 'custom_fallback',
|
||||
'src' => esc_url($url),
|
||||
'href' => $permalink,
|
||||
"type" => 'image',
|
||||
"from" => 'custom_fallback',
|
||||
"src" => esc_url($url),
|
||||
"href" => $permalink,
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -24,4 +24,4 @@ $manifest = get_template_directory_uri() . '/manifest.json';
|
||||
<style type="text/css">.is-fixed .logo,.is-fixed .metadata{position: static;}</style>
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
<body>
|
||||
<body class="theme-light">
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 444 B After Width: | Height: | Size: 444 B |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 971 KiB After Width: | Height: | Size: 971 KiB |
1
src/index.css
Normal file
@@ -0,0 +1 @@
|
||||
@import "./css/index";
|
||||
@@ -3,12 +3,12 @@
|
||||
<section class="page__logo">
|
||||
<?php get_template_part('partials/logo') ?>
|
||||
</section>
|
||||
<?php while ( have_posts() ) : the_post(); $index++; ?>
|
||||
<?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 xlink:href="#icon-calendar"></use>
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span class="article__date-text">
|
||||
<?php the_time(get_option('date_format')) ?>
|
||||
22
src/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
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();
|
||||
});
|
||||
*/
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
$posts = get_posts(array(
|
||||
numberposts => -1,
|
||||
order => 'ASC',
|
||||
"numberposts" => -1,
|
||||
"order" => 'ASC',
|
||||
));
|
||||
$archive = array();
|
||||
|
||||
@@ -14,12 +14,12 @@ foreach ($posts as $post) {
|
||||
$comments = $post->comment_count === '1' ? '1 comment' : $post->comment_count . ' comments';
|
||||
if ($post->post_status === 'publish') {
|
||||
$archive[$year][$month][] = array(
|
||||
title => $post->post_title,
|
||||
slug => $post->post_name,
|
||||
author => $post->post_author,
|
||||
day => $day,
|
||||
url => $url,
|
||||
comments => $comments,
|
||||
"title" => $post->post_title,
|
||||
"slug" => $post->post_name,
|
||||
"author" => $post->post_author,
|
||||
"day" => $day,
|
||||
"url" => $url,
|
||||
"comments" => $comments,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,21 +2,21 @@
|
||||
<div class="social__item">
|
||||
<a class="social__link" href="https://www.facebook.com/WindyCOYS/" aria-label="WindyCOYS on Facebook">
|
||||
<svg class="icon icon--facebook" aria-hidden="true">
|
||||
<use xlink:href="#icon-facebook"></use>
|
||||
<use href="#icon-facebook"></use>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="social__item">
|
||||
<a class="social__link" href="https://twitter.com/windycoys/" aria-label="WindyCOYS on Twitter">
|
||||
<svg class="icon icon--twitter" aria-hidden="true">
|
||||
<use xlink:href="#icon-twitter"></use>
|
||||
<use href="#icon-twitter"></use>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="social__item">
|
||||
<a class="social__link" href="/feed" aria-label="WindyCOYS RSS Feed">
|
||||
<svg class="icon icon--rss" aria-hidden="true">
|
||||
<use xlink:href="#icon-rss"></use>
|
||||
<use href="#icon-rss"></use>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
@@ -13,7 +13,7 @@ global $excerpt;
|
||||
<?php echo str_replace(
|
||||
'[...]',
|
||||
'<a class="link" href="' . $excerpt['url'] . '" aria-label="Read full article">'
|
||||
. '<svg class="icon icon--extra-extra-small"><use xlink:href="#icon-arrow-double-right"></use></a>',
|
||||
. '<svg class="icon icon--extra-extra-small"><use href="#icon-arrow-double-right"></use></a>',
|
||||
$excerpt['content']);
|
||||
?>
|
||||
</div>
|
||||
@@ -22,7 +22,7 @@ global $excerpt;
|
||||
<li class="excerpt__list-item">
|
||||
<span class="excerpt__icon">
|
||||
<svg class="icon icon--extra-small" aria-hidden="true">
|
||||
<use xlink:href="#icon-profile"></use>
|
||||
<use href="#icon-profile"></use>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="excerpt__text"><?php echo $excerpt['author'] ?></span>
|
||||
@@ -31,7 +31,7 @@ global $excerpt;
|
||||
<li class="excerpt__list-item">
|
||||
<span class="excerpt__icon">
|
||||
<svg class="icon icon--extra-small" aria-hidden="true">
|
||||
<use xlink:href="#icon-clock"></use>
|
||||
<use href="#icon-clock"></use>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="excerpt__text"><?php echo windycoys_get_read_time($excerpt['words']) ?> minute read</span>
|
||||
@@ -40,7 +40,7 @@ global $excerpt;
|
||||
<li class="excerpt__list-item">
|
||||
<span class="excerpt__icon">
|
||||
<svg class="icon icon--extra-small" aria-hidden="true">
|
||||
<use xlink:href="#icon-calendar"></use>
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="excerpt__text"><?php echo $excerpt['date'] ?></span>
|
||||
@@ -49,7 +49,7 @@ global $excerpt;
|
||||
<a class="excerpt__list-link" href="<?php the_permalink() ?>#comments">
|
||||
<span class="excerpt__icon">
|
||||
<svg class="icon icon--extra-small" aria-hidden="true">
|
||||
<use xlink:href="#icon-comments"></use>
|
||||
<use href="#icon-comments"></use>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="excerpt__text">
|
||||
@@ -3,7 +3,7 @@
|
||||
<li class="metadata__list-item metadata__list-item--light">
|
||||
<span class="metadata__icon">
|
||||
<svg class="icon icon--small" aria-hidden="true">
|
||||
<use xlink:href="#icon-calendar"></use>
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="metadata__text">
|
||||
@@ -15,7 +15,7 @@
|
||||
<a class="metadata__link" href="<?php echo get_edit_post_link() ?>">
|
||||
<span class="metadata__icon">
|
||||
<svg class="icon icon--small" aria-hidden="true">
|
||||
<use xlink:href="#icon-edit"></use>
|
||||
<use href="#icon-edit"></use>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="metadata__text">
|
||||
@@ -28,7 +28,7 @@
|
||||
<a class="metadata__link" href="<?php the_permalink() ?>#comments">
|
||||
<span class="metadata__icon">
|
||||
<svg class="icon icon--small" aria-hidden="true">
|
||||
<use xlink:href="#icon-comments"></use>
|
||||
<use href="#icon-comments"></use>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="metadata__text">
|
||||
@@ -39,7 +39,7 @@
|
||||
<li class="metadata__list-item">
|
||||
<span class="metadata__icon">
|
||||
<svg class="icon icon--small" aria-hidden="true">
|
||||
<use xlink:href="#icon-tag"></use>
|
||||
<use href="#icon-tag"></use>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="metadata__text">
|
||||
@@ -15,20 +15,20 @@ $menu = str_replace(
|
||||
$menu
|
||||
);
|
||||
?>
|
||||
<button class="menu" id="menu">
|
||||
<span class="menu__line"></span>
|
||||
<span class="menu__line"></span>
|
||||
<span class="menu__line"></span>
|
||||
<span class="menu__text">Open menu</span>
|
||||
</button>
|
||||
<nav class="nav<?php echo is_user_logged_in() ? ' logged-in' : '' ?>" aria-label="Main Navigation">
|
||||
<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>
|
||||
<div class="nav__content">
|
||||
<div class="nav__head">
|
||||
<h2 class="nav__title">
|
||||
<a class="nav__title-link" href="<?php get_site_url() ?>/" aria-label="Back Home" tabindex="-1">
|
||||
<span class="nav__title-link-icon">
|
||||
<svg class="icon icon--extra-large">
|
||||
<use xlink:href="#icon-windy"></use>
|
||||
<use href="#icon-windy"></use>
|
||||
</svg>
|
||||
</span>
|
||||
<img src="<?php echo get_template_directory_uri(); ?>/images/icon.png" alt="WindyCOYS logo" class="nav__title-link-image" data-load-transition="false">
|
||||
@@ -42,12 +42,12 @@ $menu = str_replace(
|
||||
<div class="nav__social">
|
||||
<a class="nav__social-link" href="https://www.facebook.com/WindyCOYS/" aria-label="WindyCOYS on Facebook">
|
||||
<svg class="icon icon--large">
|
||||
<use xlink:href="#icon-facebook"></use>
|
||||
<use href="#icon-facebook"></use>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="nav__social-link" href="https://twitter.com/windycoys/" aria-label="WindyCOYS on Twitter">
|
||||
<svg class="icon icon--large">
|
||||
<use xlink:href="#icon-twitter"></use>
|
||||
<use href="#icon-twitter"></use>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
@@ -5,10 +5,10 @@ $base_url = substr($url, 0, strpos($url, "page/"));
|
||||
$current = get_query_var('paged');
|
||||
$total = $wp_query->max_num_pages;
|
||||
$links = paginate_links(array(
|
||||
type => 'array',
|
||||
prev_next => false,
|
||||
current => $current,
|
||||
end_size => 2,
|
||||
"type" => 'array',
|
||||
"prev_next" => false,
|
||||
"current" => $current,
|
||||
"end_size" => 2,
|
||||
));
|
||||
|
||||
function get_previous_url($base_url, $page) {
|
||||
@@ -35,7 +35,7 @@ function get_next_url($base_url, $page) {
|
||||
}
|
||||
function format_link($link) {
|
||||
if (strpos($link, 'page-numbers dots')) {
|
||||
return '<span class="pagination__dots"><svg class="icon icon--extra-small"><use xlink:href="#icon-ellipsis"></use></svg></span>';
|
||||
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);
|
||||
@@ -49,7 +49,7 @@ if (count($links) > 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 xlink:href="#icon-chevron-left"></use>
|
||||
<use href="#icon-chevron-left"></use>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
@@ -67,7 +67,7 @@ if (count($links) > 1) :
|
||||
<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 xlink:href="#icon-chevron-right"></use>
|
||||
<use href="#icon-chevron-right"></use>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
@@ -40,7 +40,7 @@ $artwork = get_template_directory_uri().'/images/'.$podcast['id'].'.png';
|
||||
<li class="excerpt__list-item">
|
||||
<span class="excerpt__icon">
|
||||
<svg class="icon icon--extra-small" aria-hidden="true">
|
||||
<use xlink:href="#icon-calendar"></use>
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="excerpt__text"><?php echo $podcast['date'] ?></span>
|
||||
@@ -24,13 +24,13 @@ $results = $wp_query->found_posts;
|
||||
<li>
|
||||
<?php
|
||||
$excerpt = array(
|
||||
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)),
|
||||
"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')
|
||||
?>
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="article__date">
|
||||
<span class="article__date-icon">
|
||||
<svg class="icon icon--extra-small">
|
||||
<use xlink:href="#icon-calendar"></use>
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="article__date-text">
|
||||
@@ -7,7 +7,7 @@
|
||||
<h1 class="title">
|
||||
<span class="title__icon">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-tag"></use>
|
||||
<use href="#icon-tag"></use>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="title__text">
|
||||
@@ -19,13 +19,13 @@
|
||||
<li>
|
||||
<?php
|
||||
$excerpt = array(
|
||||
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)),
|
||||
"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');
|
||||
?>
|
||||
75
src/ts/app.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||