├── .prettierrc ├── .nvmrc ├── plugin ├── index.js ├── gatsby-ssr.js ├── gatsby-browser.js ├── gatsby-node.js ├── package.json ├── lib │ ├── utils.test.js │ ├── utils.js │ ├── snowfall.test.js │ └── snowfall.js ├── gatsby-browser.test.js └── gatsby-node.test.js ├── demo ├── gatsby-browser.js ├── src │ ├── utils │ │ └── colors.js │ ├── pages │ │ ├── 404.js │ │ ├── another.js │ │ └── index.js │ └── components │ │ └── colors.js ├── gatsby-config.js ├── package.json └── styles │ └── global.css ├── loadershim.js ├── jest-preprocess.js ├── .gitignore ├── jest.config.js ├── .github └── workflows │ └── release.yml ├── package.json ├── CONTRIBUTING.md ├── .releaserc ├── CHANGELOG.md ├── README.md └── CODE_OF_CONDUCT.md /.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /plugin/index.js: -------------------------------------------------------------------------------- 1 | //no-op 2 | -------------------------------------------------------------------------------- /demo/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | import "./styles/global.css"; 2 | -------------------------------------------------------------------------------- /loadershim.js: -------------------------------------------------------------------------------- 1 | global.___loader = { 2 | enqueue: jest.fn(), 3 | }; 4 | -------------------------------------------------------------------------------- /plugin/gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | // https://www.gatsbyjs.com/docs/reference/config-files/gatsby-ssr/ 2 | -------------------------------------------------------------------------------- /jest-preprocess.js: -------------------------------------------------------------------------------- 1 | const babelOptions = { 2 | presets: ["babel-preset-gatsby"], 3 | }; 4 | 5 | module.exports = require("babel-jest").default.createTransformer(babelOptions); 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Npm 12 | package.lock 13 | 14 | # Gatsby cache and public 15 | .cache 16 | public/ 17 | 18 | # README copied over when releasing 19 | plugin/README.md -------------------------------------------------------------------------------- /demo/src/utils/colors.js: -------------------------------------------------------------------------------- 1 | const getColor = (name) => { 2 | if (typeof getComputedStyle !== "function") return; 3 | 4 | return getComputedStyle(document.documentElement) 5 | .getPropertyValue(`--${name}`) 6 | .trim(); 7 | }; 8 | 9 | const setColor = (name, value) => { 10 | document.documentElement.style.setProperty(`--${name}`, value); 11 | }; 12 | 13 | export { getColor, setColor }; 14 | -------------------------------------------------------------------------------- /demo/gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | { 4 | resolve: "@raae/gatsby-plugin-let-it-snow", 5 | options: { 6 | colors: ["#EBDEF0", "--snow1", "--snow2", "--snow3"], 7 | duration: 30, 8 | season: { 9 | start: new Date("January 1"), 10 | end: new Date("December 31"), 11 | }, 12 | }, 13 | }, 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /plugin/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | // https://www.gatsbyjs.com/docs/reference/config-files/gatsby-browser/ 2 | 3 | import { isSeason } from "./lib/utils"; 4 | import snowfall from "./lib/snowfall"; 5 | 6 | export const onInitialClientRender = (_, pluginOptions) => { 7 | const { colors, intensity, duration, season } = pluginOptions; 8 | 9 | if (!isSeason(new Date(), season)) { 10 | return; 11 | } 12 | 13 | snowfall({ colors, intensity, duration: duration * 1000 }); 14 | }; 15 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "private": false, 5 | "description": "@raae/gatsby-plugin-let-it-snow demo", 6 | "author": "@raae", 7 | "keywords": [ 8 | "gatsby" 9 | ], 10 | "scripts": { 11 | "develop": "gatsby develop", 12 | "start": "gatsby develop", 13 | "build": "gatsby build", 14 | "serve": "gatsby serve", 15 | "clean": "gatsby clean" 16 | }, 17 | "dependencies": { 18 | "@raae/gatsby-plugin-let-it-snow": "*", 19 | "gatsby": "5.1.0", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | "^.+\\.jsx?$": `/jest-preprocess.js`, 4 | }, 5 | moduleNameMapper: { 6 | ".+\\.(css|styl|less|sass|scss)$": `identity-obj-proxy`, 7 | ".+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": `/__mocks__/file-mock.js`, 8 | }, 9 | testPathIgnorePatterns: [`node_modules`, `\\.cache`, `.*/public`], 10 | transformIgnorePatterns: [`node_modules/(?!(gatsby)/)`], 11 | globals: { 12 | __PATH_PREFIX__: ``, 13 | }, 14 | setupFiles: [`/loadershim.js`], 15 | }; 16 | -------------------------------------------------------------------------------- /demo/src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "gatsby"; 3 | 4 | const NotFoundPage = () => { 5 | return ( 6 |
7 |
8 |

Page not found

9 |

10 | Sorry{" "} 11 | 12 | 😔 13 | {" "} 14 | we couldn’t find what you were looking for. 15 |

16 | 17 | Go home 18 | 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default NotFoundPage; 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | release: 5 | name: Release 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v3 10 | with: 11 | fetch-depth: 0 12 | # Make sure the release step uses its own credentials. 13 | persist-credentials: false 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 18 18 | - name: Install dependencies 19 | run: yarn install 20 | - name: Release 21 | env: 22 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 23 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 24 | run: yarn semantic-release 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "demo", 5 | "plugin" 6 | ], 7 | "scripts": { 8 | "test": "jest --watch", 9 | "develop": "npm run develop -w demo", 10 | "semantic-release": "jest && semantic-release" 11 | }, 12 | "devDependencies": { 13 | "@semantic-release/changelog": "6.0.1", 14 | "@semantic-release/git": "10.0.1", 15 | "babel-jest": "27.4.2", 16 | "babel-preset-gatsby": "2.3.0", 17 | "gatsby-plugin-utils": "2.3.0", 18 | "identity-obj-proxy": "3.0.0", 19 | "jest": "27.4.3", 20 | "react-test-renderer": "17.0.2", 21 | "semantic-release": "^18.0.0" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/queen-raae/gatsby-plugin-let-it-snow.git" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /demo/src/pages/another.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "gatsby"; 3 | import Colors from "../components/colors"; 4 | 5 | const AnotherPage = () => { 6 | return ( 7 |
8 | 9 |
10 |

11 | 12 | ❄️  13 | 14 | Let it snow... 15 | 16 |  ❄️ 17 | 18 |
19 |

20 |

Another page, to demonstrate it only loads once!

21 | 22 | Go home 23 | 24 |
25 |
26 | ); 27 | }; 28 | 29 | export default AnotherPage; 30 | -------------------------------------------------------------------------------- /plugin/gatsby-node.js: -------------------------------------------------------------------------------- 1 | // https://www.gatsbyjs.com/docs/reference/config-files/gatsby-node/ 2 | exports.pluginOptionsSchema = ({ Joi }) => { 3 | return Joi.object({ 4 | colors: Joi.array() 5 | .items(Joi.string()) 6 | .min(1) 7 | .default(["#fff"]) 8 | .description("Array of hex color values"), 9 | intensity: Joi.string() 10 | .valid("regular", "light", "blizzard") 11 | .default("regular"), 12 | duration: Joi.number() 13 | .integer() 14 | .min(1) 15 | .default(15) 16 | .description("Duration of snowfall in seconds"), 17 | season: Joi.object() 18 | .keys({ 19 | start: Joi.date().required(), 20 | end: Joi.date().required(), 21 | }) 22 | .default({ 23 | start: new Date("December 1"), 24 | end: new Date("January 4"), 25 | }) 26 | .description("Start and end date for when snow should be activated"), 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@raae/gatsby-plugin-let-it-snow", 3 | "version": "1.1.0", 4 | "description": "Add some old school cheer to your site with falling snow this holiday season ❄️❄️❄️", 5 | "main": "index.js", 6 | "scripts": { 7 | "postversion": "cp ../README.md ./README.md" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/queen-raae/gatsby-plugin-let-it-snow.git" 12 | }, 13 | "author": "@raae", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/queen-raae/gatsby-plugin-let-it-snow/issues" 17 | }, 18 | "homepage": "https://github.com/raae/gatsby-plugin-let-it-snow#readme", 19 | "keywords": [ 20 | "gatsby", 21 | "gatsby-plugin" 22 | ], 23 | "dependencies": { 24 | "canvas-confetti": "1.4.0", 25 | "date-fns": "2.27.0" 26 | }, 27 | "peerDependencies": { 28 | "gatsby": "^2.4.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" 29 | }, 30 | "engines": { 31 | "node": "12.x || 14.x || 16.x || 18.x" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute with code 2 | 3 | ### Getting started 4 | 5 | - Fork the repo 6 | - Clone your fork 7 | - Install the packages: `yarn install` 8 | - Run the demo: `yarn develop` 9 | 10 | To be able to have both the demo and plugin in the same repo we use [yarn workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/). 11 | 12 | - Plugin code is in the `/plugin` folder, 13 | - and the demo code is in the `/demo` folder. 14 | 15 | ### Do your thing 16 | 17 | Solve an [issue](https://github.com/queen-raae/gatsby-plugin-let-it-snow/issues) or do something else! 18 | 19 | ### Create a Pull Request 20 | 21 | To get your changes merged into the project create a Pull Request from your fork. 22 | 23 | ## How to contribute in other ways 24 | 25 | ### Questions, Feedback and Suggestions 26 | 27 | If you have any questions, feedback or suggestions head on over to [discussions](https://github.com/queen-raae/gatsby-plugin-let-it-snow/discussions). 28 | 29 | ### Bugs 30 | 31 | If you find a bug please open an [issue](https://github.com/queen-raae/gatsby-plugin-let-it-snow/issues) and/or create a pull request to fix it. 32 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main", 4 | { 5 | "name": "next", 6 | "prerelease": true 7 | } 8 | ], 9 | "plugins": [ 10 | [ 11 | "@semantic-release/commit-analyzer", 12 | { 13 | "preset": "angular", 14 | "releaseRules": [ 15 | { 16 | "type": "docs", 17 | "scope": "README", 18 | "release": "patch" 19 | } 20 | ], 21 | "parserOpts": { 22 | "noteKeywords": [ 23 | "BREAKING CHANGE", 24 | "BREAKING CHANGES" 25 | ] 26 | } 27 | } 28 | ], 29 | "@semantic-release/release-notes-generator", 30 | [ 31 | "@semantic-release/changelog", 32 | { 33 | "changelogFile": "CHANGELOG.md" 34 | } 35 | ], 36 | [ 37 | "@semantic-release/npm", 38 | { 39 | "pkgRoot": "plugin" 40 | } 41 | ], 42 | [ 43 | "@semantic-release/git", 44 | { 45 | "assets": [ 46 | "plugin/package.json", 47 | "CHANGELOG.md" 48 | ] 49 | } 50 | ], 51 | "@semantic-release/github" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /plugin/lib/utils.test.js: -------------------------------------------------------------------------------- 1 | import { isSeason } from "./utils"; 2 | 3 | describe("isSeason", () => { 4 | it("within season even with mismatching years", () => { 5 | expect( 6 | isSeason(new Date("December 1"), { 7 | start: "2021-12-01", 8 | end: "2026-01-10", 9 | }) 10 | ).toBe(true); 11 | 12 | expect( 13 | isSeason(new Date("January 1"), { 14 | start: "2021-12-01", 15 | end: "2026-01-10", 16 | }) 17 | ).toBe(true); 18 | 19 | expect( 20 | isSeason(new Date("May 17"), { 21 | start: "2021-05-01", 22 | end: "2026-06-10", 23 | }) 24 | ).toBe(true); 25 | }); 26 | 27 | it("outside season even with mismatching years", () => { 28 | expect( 29 | isSeason(new Date("November 30"), { 30 | start: "2021-12-01", 31 | end: "2026-01-10", 32 | }) 33 | ).toBe(false); 34 | 35 | expect( 36 | isSeason(new Date("January 11"), { 37 | start: "2021-12-01", 38 | end: "2026-01-10", 39 | }) 40 | ).toBe(false); 41 | 42 | expect( 43 | isSeason(new Date("July 10"), { 44 | start: "2021-05-01", 45 | end: "2026-06-10", 46 | }) 47 | ).toBe(false); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /plugin/lib/utils.js: -------------------------------------------------------------------------------- 1 | import { 2 | addYears, 3 | getYear, 4 | setYear, 5 | isBefore, 6 | isWithinInterval, 7 | parseISO, 8 | } from "date-fns"; 9 | 10 | export const randomInRange = (min, max) => { 11 | return Math.random() * (max - min) + min; 12 | }; 13 | 14 | export const isSeason = (date, { start, end }) => { 15 | try { 16 | const currentYear = getYear(date); 17 | 18 | // Ignore year from config dates 19 | const startDate = setYear(parseISO(start), currentYear); 20 | let endDate = setYear(parseISO(end), currentYear); 21 | 22 | if (isBefore(endDate, startDate)) { 23 | endDate = addYears(endDate, 1); 24 | } 25 | 26 | const interval = { 27 | start: startDate, 28 | end: endDate, 29 | }; 30 | 31 | return ( 32 | isWithinInterval(date, interval) || 33 | isWithinInterval(addYears(date, 1), interval) 34 | ); 35 | } catch (error) { 36 | console.warn( 37 | "Problem with @raae/gatsby-plugin-let-it-snow season configuration:", 38 | error.message 39 | ); 40 | return false; 41 | } 42 | }; 43 | 44 | export const getCssVariable = (color) => { 45 | return getComputedStyle(document.documentElement) 46 | .getPropertyValue(color) 47 | .trim(); 48 | }; 49 | -------------------------------------------------------------------------------- /demo/src/components/colors.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { getColor, setColor } from "../utils/colors"; 3 | 4 | const Colors = () => { 5 | const [colors, setColors] = useState({ 6 | snow1: getColor("snow1"), 7 | snow2: getColor("snow2"), 8 | }); 9 | 10 | useEffect(() => { 11 | const mql = window.matchMedia("(prefers-color-scheme: dark)"); 12 | 13 | const onChange = () => { 14 | setColors({ 15 | snow1: getColor("snow1"), 16 | snow2: getColor("snow2"), 17 | }); 18 | }; 19 | 20 | mql.addEventListener("change", onChange); 21 | 22 | return () => mql.removeEventListener("change", onChange); 23 | }, []); 24 | 25 | const onChange = (event) => { 26 | setColors({ 27 | ...colors, 28 | [event.target.name]: event.target.value, 29 | }); 30 | 31 | setColor(event.target.name, event.target.value); 32 | }; 33 | 34 | return ( 35 |
36 | {Object.entries(colors).map(([key, value]) => { 37 | return ( 38 | 42 | ); 43 | })} 44 |
45 | ); 46 | }; 47 | 48 | export default Colors; 49 | -------------------------------------------------------------------------------- /demo/styles/global.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-bg: #fffaf0; 3 | --color: #412f20; 4 | --snow1: #aed6f1; 5 | } 6 | 7 | @media (prefers-color-scheme: dark) { 8 | :root { 9 | --color-bg: #412f20; 10 | --color: #fffaf0; 11 | --snow1: #e74c3c; 12 | --snow2: #ffffff; 13 | } 14 | } 15 | 16 | * { 17 | box-sizing: border-box; 18 | } 19 | 20 | html { 21 | background-color: var(--color-bg); 22 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 23 | Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 24 | color: var(--color); 25 | font-size: calc(100% + 0.5vw); 26 | } 27 | 28 | body { 29 | margin: 0; 30 | padding: 0; 31 | } 32 | 33 | main { 34 | border-top: 5px solid #ec4326; 35 | min-height: 100vh; 36 | display: flex; 37 | flex-direction: column; 38 | justify-content: center; 39 | } 40 | 41 | section, 42 | footer { 43 | text-align: center; 44 | padding: 1em; 45 | } 46 | 47 | a { 48 | color: inherit; 49 | } 50 | 51 | h1 { 52 | font-size: 2em; 53 | font-weight: 900; 54 | } 55 | 56 | p { 57 | max-width: 48ch; 58 | margin: 1em auto; 59 | line-height: 1.5; 60 | } 61 | 62 | span[role="img"] { 63 | padding: 0 0.15em; 64 | } 65 | 66 | .colors { 67 | position: absolute; 68 | top: 0; 69 | left: 0; 70 | display: flex; 71 | flex-direction: column; 72 | row-gap: 0.5em; 73 | } 74 | -------------------------------------------------------------------------------- /demo/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "gatsby"; 3 | import Colors from "../components/colors"; 4 | 5 | const IndexPage = () => { 6 | return ( 7 |
8 | 9 |
10 |

11 | 12 |  ❄️  13 | 14 | Let it snow... 15 | 16 |  ❄️  17 | 18 |
19 |

20 |

21 | Want to sprinkle some old school holiday joy on your own page? Install{" "} 22 | 23 | @raae/gatsby-plugin-let-it-snow! 24 | 25 |

26 |

27 | 28 | Go to another page and notice it's only 29 | initalized once. 30 | 31 |

32 |
33 | 46 |
47 | ); 48 | }; 49 | 50 | export default IndexPage; 51 | -------------------------------------------------------------------------------- /plugin/lib/snowfall.test.js: -------------------------------------------------------------------------------- 1 | import confetti from "canvas-confetti"; 2 | import snowfall, { animationFrame } from "./snowfall"; 3 | import { getCssVariable } from "./utils"; 4 | 5 | jest.mock("./utils", () => { 6 | return { 7 | ...jest.requireActual("./utils"), 8 | getCssVariable: jest.fn(), 9 | }; 10 | }); 11 | jest.mock("canvas-confetti"); 12 | global.requestAnimationFrame = jest.fn((cb) => cb()); 13 | 14 | const OPTIONS = { 15 | colors: ["fff", "--snow-color-1", "--snow-color-2"], 16 | intensity: "blizzard", 17 | duration: 1, 18 | }; 19 | 20 | describe("snowfall", () => { 21 | afterEach(() => { 22 | jest.clearAllMocks(); 23 | }); 24 | 25 | it("not an infinite loop", () => { 26 | // Will exeed maximum stack if infinite 27 | snowfall(OPTIONS); 28 | expect(confetti).toBeCalled(); 29 | expect(global.requestAnimationFrame).toBeCalled(); 30 | }); 31 | 32 | describe("animationFrame", () => { 33 | it("when time left call confetti, recurrsion and get css variables", () => { 34 | animationFrame({ animationEnd: 6, ...OPTIONS }, 5); 35 | 36 | expect(confetti).toBeCalledTimes(1); 37 | expect(global.requestAnimationFrame).toBeCalledTimes(1); 38 | expect(getCssVariable).toBeCalledTimes(2); 39 | }); 40 | 41 | it("when no time left do not call confetti and recurrsion", () => { 42 | animationFrame({ animationEnd: 5, ...OPTIONS }, 5); 43 | 44 | expect(confetti).toBeCalledTimes(0); 45 | expect(global.requestAnimationFrame).toBeCalledTimes(0); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /plugin/gatsby-browser.test.js: -------------------------------------------------------------------------------- 1 | import { addDays } from "date-fns"; 2 | import snowfall from "./lib/snowfall"; 3 | import { onInitialClientRender } from "./gatsby-browser"; 4 | 5 | jest.mock("./lib/snowfall"); 6 | 7 | describe("onInitialClientRender", () => { 8 | afterEach(() => { 9 | jest.clearAllMocks(); 10 | }); 11 | 12 | describe("season option", () => { 13 | it("activates snowfall when in season", () => { 14 | const today = new Date(); 15 | const pluginOptions = { 16 | colors: ["fff"], 17 | intensity: "blizzard", 18 | duration: 10, 19 | season: { 20 | start: addDays(today, -1).toISOString(), 21 | end: addDays(today, +1).toISOString(), 22 | }, 23 | }; 24 | 25 | onInitialClientRender(null, pluginOptions); 26 | 27 | expect(snowfall).toBeCalledTimes(1); 28 | expect(snowfall).toBeCalledWith({ 29 | colors: ["fff"], 30 | intensity: "blizzard", 31 | duration: 10 * 1000, 32 | }); 33 | }); 34 | 35 | it("does not activate snowfall outside the season", () => { 36 | const today = new Date(); 37 | const pluginOptions = { 38 | colors: ["fff"], 39 | intensity: "blizzard", 40 | duration: 10, 41 | season: { 42 | start: addDays(today, -10).toISOString(), 43 | end: addDays(today, -1).toISOString(), 44 | }, 45 | }; 46 | 47 | onInitialClientRender(null, pluginOptions); 48 | 49 | expect(snowfall).toBeCalledTimes(0); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /plugin/lib/snowfall.js: -------------------------------------------------------------------------------- 1 | import confetti from "canvas-confetti"; 2 | import { randomInRange, getCssVariable } from "./utils"; 3 | 4 | export const animationFrame = (options, currentTimestamp = Date.now()) => { 5 | const { animationEnd, duration, skew = 1 } = options; 6 | const { colors, intensity } = options; 7 | 8 | const timeLeft = animationEnd - currentTimestamp; 9 | const newSkew = Math.max(0.8, skew - 0.001); 10 | const ticks = Math.max(200, 500 * (timeLeft / duration)); 11 | 12 | if (timeLeft <= 0) { 13 | return; 14 | } 15 | 16 | const currentColors = colors 17 | .map((color) => { 18 | if (color.startsWith("--")) { 19 | return getCssVariable(color); 20 | } else { 21 | return color; 22 | } 23 | }) 24 | .filter((color) => !!color); 25 | 26 | const intensityValues = { 27 | startVelocity: 0, 28 | gravity: randomInRange(0.4, 0.6), 29 | scalar: randomInRange(0.4, 1), 30 | drift: randomInRange(-0.4, 0.4), 31 | }; 32 | 33 | if (intensity === "blizzard") { 34 | intensityValues.gravity = randomInRange(0.4, 5); 35 | intensityValues.drift = randomInRange(-0.4, 20); 36 | } 37 | 38 | if (intensity === "light") { 39 | intensityValues.gravity = randomInRange(0.1, 0.2); 40 | intensityValues.scalar = randomInRange(0.4, 0.6); 41 | intensityValues.drift = randomInRange(0, 0); 42 | } 43 | 44 | confetti({ 45 | particleCount: 1, 46 | startVelocity: 0, 47 | ticks: ticks, 48 | origin: { 49 | x: Math.random(), 50 | // since particles fall down, skew start toward the top 51 | y: Math.random() * newSkew - 0.2, 52 | }, 53 | colors: [currentColors[Math.floor(randomInRange(0, currentColors.length))]], 54 | shapes: ["circle"], 55 | ...intensityValues, 56 | disableForReducedMotion: true, 57 | }); 58 | 59 | requestAnimationFrame(() => { 60 | animationFrame({ ...options }); 61 | }); 62 | }; 63 | 64 | export default (options) => { 65 | const now = Date.now(); 66 | 67 | animationFrame({ 68 | ...options, 69 | animationEnd: now + options.duration, 70 | }); 71 | }; 72 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [1.1.0](https://github.com/queen-raae/gatsby-plugin-let-it-snow/compare/v1.0.2...v1.1.0) (2022-11-24) 2 | 3 | 4 | ### Features 5 | 6 | * support for v5 ([#39](https://github.com/queen-raae/gatsby-plugin-let-it-snow/issues/39)) ([8f7ac9b](https://github.com/queen-raae/gatsby-plugin-let-it-snow/commit/8f7ac9b4820a798844ec6d59374f933284dd850e)) 7 | 8 | ## [1.0.2](https://github.com/queen-raae/gatsby-plugin-let-it-snow/compare/v1.0.1...v1.0.2) (2022-02-08) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * add description to plugin package file ([ea0a313](https://github.com/queen-raae/gatsby-plugin-let-it-snow/commit/ea0a313de9f1e484a524eb5bb080db57a8123982)) 14 | 15 | ## [1.0.1](https://github.com/queen-raae/gatsby-plugin-let-it-snow/compare/v1.0.0...v1.0.1) (2022-02-08) 16 | 17 | # [1.0.0](https://github.com/queen-raae/gatsby-plugin-let-it-snow/compare/v0.6.1...v1.0.0) (2021-12-24) 18 | 19 | 20 | ### Documentation 21 | 22 | * update ([#38](https://github.com/queen-raae/gatsby-plugin-let-it-snow/issues/38)) ([1bebb34](https://github.com/queen-raae/gatsby-plugin-let-it-snow/commit/1bebb34e48b0ef0010feb1693058e22edba62ef2)) 23 | 24 | 25 | ### BREAKING CHANGES 26 | 27 | * Happy Holidays 28 | 29 | ## [0.6.1](https://github.com/queen-raae/gatsby-plugin-let-it-snow/compare/v0.6.0...v0.6.1) (2021-12-24) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * change min duration from 5 to 1 ([#37](https://github.com/queen-raae/gatsby-plugin-let-it-snow/issues/37)) ([46b8d62](https://github.com/queen-raae/gatsby-plugin-let-it-snow/commit/46b8d62543329bc17e559aa43e8b531c102d9e2d)) 35 | 36 | # [0.6.0](https://github.com/queen-raae/gatsby-plugin-let-it-snow/compare/v0.5.1...v0.6.0) (2021-12-07) 37 | 38 | 39 | ### Features 40 | 41 | * css var support, closes [#30](https://github.com/queen-raae/gatsby-plugin-let-it-snow/issues/30) ([#31](https://github.com/queen-raae/gatsby-plugin-let-it-snow/issues/31)) ([3f79993](https://github.com/queen-raae/gatsby-plugin-let-it-snow/commit/3f79993b50c5202bd1944251b7a2ad135b8689e0)) 42 | 43 | ## [0.5.1](https://github.com/queen-raae/gatsby-plugin-let-it-snow/compare/v0.5.0...v0.5.1) (2021-12-05) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * season bug, closes [#26](https://github.com/queen-raae/gatsby-plugin-let-it-snow/issues/26) ([#27](https://github.com/queen-raae/gatsby-plugin-let-it-snow/issues/27)) ([8eaa3fb](https://github.com/queen-raae/gatsby-plugin-let-it-snow/commit/8eaa3fb57d7d5161c1eaaa80df11e299f189c952)) 49 | 50 | # [0.5.0](https://github.com/queen-raae/gatsby-plugin-let-it-snow/compare/v0.4.1...v0.5.0) (2021-12-03) 51 | 52 | 53 | ### Features 54 | 55 | * add duration option, closes [#4](https://github.com/queen-raae/gatsby-plugin-let-it-snow/issues/4) ([#16](https://github.com/queen-raae/gatsby-plugin-let-it-snow/issues/16)) ([e9478fa](https://github.com/queen-raae/gatsby-plugin-let-it-snow/commit/e9478fa85f195ad839b19f9a51616372d462ef64)) 56 | 57 | ## [0.4.1](https://github.com/queen-raae/gatsby-plugin-let-it-snow/compare/v0.4.0...v0.4.1) (2021-12-03) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * duration broken by [#11](https://github.com/queen-raae/gatsby-plugin-let-it-snow/issues/11) ([616a01f](https://github.com/queen-raae/gatsby-plugin-let-it-snow/commit/616a01f1cbadca3bd7d39e7e92bc01f5c157c036)) 63 | 64 | # [0.4.0](https://github.com/queen-raae/gatsby-plugin-let-it-snow/compare/v0.3.0...v0.4.0) (2021-12-03) 65 | 66 | 67 | ### Features 68 | 69 | * season-plugin-option, solves [#2](https://github.com/queen-raae/gatsby-plugin-let-it-snow/issues/2) ([#11](https://github.com/queen-raae/gatsby-plugin-let-it-snow/issues/11)) ([9319fa0](https://github.com/queen-raae/gatsby-plugin-let-it-snow/commit/9319fa0c8685ce4db85ec1af4d0419b263e2ec92)) 70 | 71 | # [0.3.0](https://github.com/queen-raae/gatsby-plugin-let-it-snow/compare/v0.2.0...v0.3.0) (2021-11-24) 72 | 73 | 74 | ### Features 75 | 76 | * adding intensity of snow, closes [#3](https://github.com/queen-raae/gatsby-plugin-let-it-snow/issues/3) ([#17](https://github.com/queen-raae/gatsby-plugin-let-it-snow/issues/17)) ([90d5808](https://github.com/queen-raae/gatsby-plugin-let-it-snow/commit/90d5808163d97267e6b678588b5d1d2f37b995af)) 77 | 78 | # [0.2.0](https://github.com/queen-raae/gatsby-plugin-let-it-snow/compare/v0.1.0...v0.2.0) (2021-11-21) 79 | 80 | 81 | ### Features 82 | 83 | * Add "Message from Queen Raae" ([48a120c](https://github.com/queen-raae/gatsby-plugin-let-it-snow/commit/48a120cddc1d49debd253dd78b77306d7db7b613)) 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @raae/gatsby-plugin-let-it-snow-test 2 | 3 | _Add some old school cheer to your site with falling snow this holiday season_ ❄️❄️❄️ 4 | 5 | Heavily inspired by my memory of snow falling on my WordPress blog way back when... 6 | 7 |   8 | 9 | ## A message or two or three from Queen Raae 👑 10 | 11 | ### 1-on-1 Emergency Gatsby Call 12 | 13 | Are you stuck on a reef in the sharky waters around the Gatsby islands? Check out [1-on-1 Emergency Gatsby Call](https://queen.raae.codes/gatsby-emergency/) with Queen Raae to get friendly advice you can put into action immediately from a seasoned Gatsby developer. 14 | 15 | ### Stay updated and get the most out of Gatsby 16 | 17 | Learn how to get the most out of Gatsby and **stay updated** on the template by [subscribing](https://queen.raae.codes/emails/?utm_source=readme&utm_campaign=let-it-snow) to daily emails from Queen Raae and Cap'n Ola. 18 | 19 | ### Video Walkthrough 20 | 21 | Watch Cap'n Ola add snow to his site on an unauthorized and rum-fueled [treasure hunt](https://youtu.be/eaZm9MC0GeE) in the sharky waters around the Gatsby islands on [YouTube](https://youtu.be/zRUxnx7pv0E). 22 | 23 |   24 | 25 | ## How to install 26 | 27 | `npm install @raae/gatsby-plugin-let-it-snow` 28 | 29 | or 30 | 31 | `yarn add @raae/gatsby-plugin-let-it-snow` 32 | 33 | ## How to use 34 | 35 | Add the plugin to the plugins array in your `gatsby.config`. 36 | 37 | ``` 38 | // gatsby.config.js 39 | 40 | module.exports = { 41 | plugins: [ 42 | `@raae/gatsby-plugin-let-it-snow` 43 | ], 44 | } 45 | ``` 46 | 47 | ## Plugin Options 48 | 49 | ### Colors 50 | 51 | You can decide the color of the snow flakes. Each snowflake gets a random color from the list. 52 | **Type:** An array of hex colors, or css variable names resolving to hex colors. 53 | **Default:** `["#fff"]` 54 | 55 | ``` 56 | // gatsby.config.js 57 | 58 | module.exports = { 59 | plugins: [ 60 | { 61 | resolve: "@raae/gatsby-plugin-let-it-snow", 62 | options: { 63 | colors: ["#fff000", "--snow-color-1", "--snow-color-2"], 64 | }, 65 | }, 66 | ], 67 | }; 68 | ``` 69 | 70 | ### Intensity 71 | 72 | You can choose the intensity of the snow. 73 | **Type**: One of "light", "regular" or "blizzard". 74 | **Default:** `"regular"` 75 | 76 | ``` 77 | // gatsby.config.js 78 | 79 | module.exports = { 80 | plugins: [ 81 | { 82 | resolve: "@raae/gatsby-plugin-let-it-snow", 83 | options: { 84 | intensity: "blizzard", 85 | }, 86 | }, 87 | ], 88 | }; 89 | ``` 90 | 91 | ### Duration 92 | 93 | You can decide how long the snow should fall for. 94 | **Type**: Positive integer 95 | **Default:** `15` 96 | 97 | ``` 98 | // gatsby.config.js 99 | 100 | module.exports = { 101 | plugins: [ 102 | { 103 | resolve: "@raae/gatsby-plugin-let-it-snow", 104 | options: { 105 | duration: 10, 106 | }, 107 | }, 108 | ], 109 | }; 110 | ``` 111 | 112 | ### Season 113 | 114 | You may decide the time of year the snow should fall. 115 | **Type**: Dates, but year will be ignored. 116 | **Default:** `{ start: new Date("December 1"), end: new Date("January 4") }` 117 | **Important:** Year will be ignored, and snow will fall each year within the configured season. 118 | 119 | ``` 120 | // gatsby.config.js 121 | 122 | module.exports = { 123 | plugins: [ 124 | { 125 | resolve: "@raae/gatsby-plugin-let-it-snow", 126 | options: { 127 | season: { 128 | start: new Date("November 15"), 129 | end: new Date("January 15"), 130 | }, 131 | }, 132 | }, 133 | ], 134 | }; 135 | ``` 136 | 137 | ## Powered by Canvas Confetti 🎉 138 | 139 | Under the hood this plugin uses the amazing [Canvas Confetti](https://github.com/catdad/canvas-confetti) by [@kirilv](https://twitter.com/kirilv). 140 | 141 | ## How to contribute 142 | 143 | If you would like to contribute with code, check out `CONTRIBUTING.md`. 144 | 145 | ### Questions, Feedback and Suggestions 146 | 147 | Do not be shy! 148 | 149 | If you have any questions, feedback or suggestions head on over to [discussions](https://github.com/queen-raae/gatsby-plugin-let-it-snow/discussions). 150 | 151 | ### Bugs 152 | 153 | If you find a bug please open an [issue](https://github.com/raae/gatsby-plugin-let-it-snow/issues) and/or create a pull request to fix it. 154 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at [queen@raae.codes](mailto://queen@raae.codes). 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series of 85 | actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or permanent 92 | ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within the 112 | community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.1, available at 118 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 119 | 120 | Community Impact Guidelines were inspired by 121 | [Mozilla's code of conduct enforcement ladder][mozilla coc]. 122 | 123 | For answers to common questions about this code of conduct, see the FAQ at 124 | [https://www.contributor-covenant.org/faq][faq]. Translations are available at 125 | [https://www.contributor-covenant.org/translations][translations]. 126 | 127 | [homepage]: https://www.contributor-covenant.org 128 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 129 | [mozilla coc]: https://github.com/mozilla/diversity 130 | [faq]: https://www.contributor-covenant.org/faq 131 | [translations]: https://www.contributor-covenant.org/translations 132 | -------------------------------------------------------------------------------- /plugin/gatsby-node.test.js: -------------------------------------------------------------------------------- 1 | // This is an example using Jest (https://jestjs.io/) 2 | import { testPluginOptionsSchema } from "gatsby-plugin-utils"; 3 | import { pluginOptionsSchema } from "./gatsby-node"; 4 | 5 | describe("pluginOptionsSchema", () => { 6 | it("allows no configuration", async () => { 7 | const { isValid } = await testPluginOptionsSchema(pluginOptionsSchema); 8 | expect(isValid).toBe(true); 9 | }); 10 | 11 | describe("colors", () => { 12 | it("valid when array of strings", async () => { 13 | const options = { 14 | colors: ["#222"], 15 | }; 16 | const { isValid } = await testPluginOptionsSchema( 17 | pluginOptionsSchema, 18 | options 19 | ); 20 | 21 | expect(isValid).toBe(true); 22 | }); 23 | 24 | it("invalid when non array", async () => { 25 | const options = { 26 | colors: "#666", 27 | }; 28 | const { isValid, errors } = await testPluginOptionsSchema( 29 | pluginOptionsSchema, 30 | options 31 | ); 32 | 33 | expect(isValid).toBe(false); 34 | expect(errors).toEqual([`"colors" must be an array`]); 35 | }); 36 | 37 | it("invalid when array of non strings", async () => { 38 | const options = { 39 | colors: [666], 40 | }; 41 | const { isValid, errors } = await testPluginOptionsSchema( 42 | pluginOptionsSchema, 43 | options 44 | ); 45 | 46 | expect(isValid).toBe(false); 47 | expect(errors).toEqual([`"colors[0]" must be a string`]); 48 | }); 49 | }); 50 | 51 | describe("intensity", () => { 52 | it("valid for: light", async () => { 53 | const options = { 54 | intensity: "light", 55 | }; 56 | const { isValid } = await testPluginOptionsSchema( 57 | pluginOptionsSchema, 58 | options 59 | ); 60 | 61 | expect(isValid).toBe(true); 62 | }); 63 | 64 | it("valid for: regular", async () => { 65 | const options = { 66 | intensity: "regular", 67 | }; 68 | const { isValid } = await testPluginOptionsSchema( 69 | pluginOptionsSchema, 70 | options 71 | ); 72 | 73 | expect(isValid).toBe(true); 74 | }); 75 | 76 | it("valid for: blizzard", async () => { 77 | const options = { 78 | intensity: "blizzard", 79 | }; 80 | const { isValid } = await testPluginOptionsSchema( 81 | pluginOptionsSchema, 82 | options 83 | ); 84 | 85 | expect(isValid).toBe(true); 86 | }); 87 | 88 | it("invalid for other string", async () => { 89 | const options = { 90 | intensity: "test", 91 | }; 92 | const { isValid, errors } = await testPluginOptionsSchema( 93 | pluginOptionsSchema, 94 | options 95 | ); 96 | 97 | expect(isValid).toBe(false); 98 | expect(errors).toEqual([ 99 | `"intensity" must be one of [regular, light, blizzard]`, 100 | ]); 101 | }); 102 | 103 | it("invalid for non string", async () => { 104 | const options = { 105 | intensity: 666, 106 | }; 107 | const { isValid, errors } = await testPluginOptionsSchema( 108 | pluginOptionsSchema, 109 | options 110 | ); 111 | 112 | expect(isValid).toBe(false); 113 | expect(errors).toEqual([ 114 | `"intensity" must be one of [regular, light, blizzard]`, 115 | `"intensity" must be a string`, 116 | ]); 117 | }); 118 | }); 119 | 120 | describe("duration", () => { 121 | it("valid for int over 0", async () => { 122 | const options = { 123 | duration: 10, 124 | }; 125 | const { isValid } = await testPluginOptionsSchema( 126 | pluginOptionsSchema, 127 | options 128 | ); 129 | 130 | expect(isValid).toBe(true); 131 | }); 132 | 133 | it("valid for int string over 0", async () => { 134 | const options = { 135 | duration: "16", 136 | }; 137 | const { isValid } = await testPluginOptionsSchema( 138 | pluginOptionsSchema, 139 | options 140 | ); 141 | 142 | expect(isValid).toBe(true); 143 | }); 144 | 145 | it("invalid for int under 1", async () => { 146 | const options = { 147 | duration: 0, 148 | }; 149 | const { isValid, errors } = await testPluginOptionsSchema( 150 | pluginOptionsSchema, 151 | options 152 | ); 153 | 154 | expect(isValid).toBe(false); 155 | expect(errors).toEqual([`"duration" must be greater than or equal to 1`]); 156 | }); 157 | 158 | it("invalid for float", async () => { 159 | const options = { 160 | duration: 3.4, 161 | }; 162 | const { isValid, errors } = await testPluginOptionsSchema( 163 | pluginOptionsSchema, 164 | options 165 | ); 166 | 167 | expect(isValid).toBe(false); 168 | expect(errors).toEqual([`"duration" must be an integer`]); 169 | }); 170 | 171 | it("invalid for non int string", async () => { 172 | const options = { 173 | duration: "test", 174 | }; 175 | const { isValid, errors } = await testPluginOptionsSchema( 176 | pluginOptionsSchema, 177 | options 178 | ); 179 | 180 | expect(isValid).toBe(false); 181 | expect(errors).toEqual([`"duration" must be a number`]); 182 | }); 183 | }); 184 | 185 | describe("season", () => { 186 | it("valid for correct season", async () => { 187 | const options = { 188 | season: { 189 | start: "2021-12-04", 190 | end: new Date("January 4"), 191 | }, 192 | }; 193 | const { isValid } = await testPluginOptionsSchema( 194 | pluginOptionsSchema, 195 | options 196 | ); 197 | 198 | expect(isValid).toBe(true); 199 | }); 200 | 201 | it("invalid for missing start/end", async () => { 202 | const options = { 203 | season: {}, 204 | }; 205 | 206 | const { isValid, errors } = await testPluginOptionsSchema( 207 | pluginOptionsSchema, 208 | options 209 | ); 210 | 211 | expect(isValid).toBe(false); 212 | expect(errors).toEqual([ 213 | `"season.start" is required`, 214 | `"season.end" is required`, 215 | ]); 216 | }); 217 | 218 | it("invalid for non dates", async () => { 219 | const options = { 220 | season: { 221 | start: "test", 222 | end: "2013-13-01", 223 | }, 224 | }; 225 | 226 | const { isValid, errors } = await testPluginOptionsSchema( 227 | pluginOptionsSchema, 228 | options 229 | ); 230 | 231 | expect(isValid).toBe(false); 232 | expect(errors).toEqual([ 233 | `"season.start" must be a valid date`, 234 | `"season.end" must be a valid date`, 235 | ]); 236 | }); 237 | }); 238 | }); 239 | --------------------------------------------------------------------------------