├── .github └── main.workflow ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── action └── entrypoint.sh ├── docs ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── now.json ├── package.json └── src │ ├── components │ ├── Anchor.js │ ├── Button │ │ ├── AnchorButton.js │ │ ├── Button.js │ │ ├── ButtonContainer.js │ │ ├── GlowingAnchorButton.js │ │ ├── GlowingButton.js │ │ └── LinkButton.js │ ├── CenteredText.js │ ├── CenteredTitle.js │ ├── Favicons.js │ ├── Feature.js │ ├── Features.js │ ├── FocusStyles.js │ ├── Footer.js │ ├── Header.js │ ├── Link.js │ └── Section.js │ ├── images │ ├── ChangeCast.png │ ├── ChangeCastTransparent.png │ └── oleg-laptev-546607-unsplash.png │ ├── styles │ ├── global.js │ └── typography.js │ └── templates │ └── IndexTemplate.js ├── fonts ├── Inter-SemiBold.woff └── fonts.css ├── icons ├── AbstractIcon1.js ├── AbstractIcon10.js ├── AbstractIcon11.js ├── AbstractIcon12.js ├── AbstractIcon2.js ├── AbstractIcon3.js ├── AbstractIcon4.js ├── AbstractIcon5.js ├── AbstractIcon6.js ├── AbstractIcon7.js ├── AbstractIcon8.js ├── AbstractIcon9.js ├── Cast.js ├── ChevronLeft.js ├── Clipboard.js ├── Close.js ├── Copy.js ├── ExternalLink.js ├── Facebook.js ├── Link.js ├── Linkedin.js ├── Radio.js ├── Search.js ├── Twitter.js ├── package.json ├── svgs │ ├── AbstractIcon1.svg │ ├── AbstractIcon10.svg │ ├── AbstractIcon11.svg │ ├── AbstractIcon12.svg │ ├── AbstractIcon2.svg │ ├── AbstractIcon3.svg │ ├── AbstractIcon4.svg │ ├── AbstractIcon5.svg │ ├── AbstractIcon6.svg │ ├── AbstractIcon7.svg │ ├── AbstractIcon8.svg │ ├── AbstractIcon9.svg │ ├── Cast.svg │ ├── ChevronLeft.svg │ ├── Clipboard.svg │ ├── Close.svg │ ├── Copy.svg │ ├── ExternalLink.svg │ ├── Facebook.svg │ ├── Link.svg │ ├── Linkedin.svg │ ├── Radio.svg │ ├── Search.svg │ └── Twitter.svg ├── templates │ └── namedExportNoSvg.js └── yarn.lock ├── netlify.toml ├── now.json ├── package.json ├── plugins ├── gatsby-remark-images │ ├── .babelrc │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── constants.js │ ├── gatsby-browser.js │ ├── index.js │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── index.js.snap │ │ │ └── index.js │ │ ├── constants.js │ │ ├── gatsby-browser.js │ │ └── index.js │ └── yarn.lock ├── gatsby-source-github-releases │ ├── gatsby-node.js │ ├── index.js │ ├── package.json │ └── yarn.lock ├── gatsby-transformer-color-thief │ ├── gatsby-node.js │ ├── index.js │ ├── package.json │ └── yarn.lock ├── gatsby-transformer-favicons │ ├── gatsby-node.js │ ├── index.js │ ├── package.json │ └── yarn.lock └── gatsby-transformer-og-image │ ├── gatsby-node.js │ ├── index.js │ ├── package.json │ └── yarn.lock ├── site ├── gatsby-config.js ├── gatsby-node.js ├── package.json └── src │ ├── components │ ├── Button │ │ ├── AnchorButton.js │ │ ├── Button.js │ │ ├── LinkButton.js │ │ └── MenuButton.js │ ├── Favicons.js │ ├── FocusStyles.js │ ├── Header.js │ ├── Release │ │ ├── Release.js │ │ ├── ReleaseHeader.js │ │ └── SocialButton.js │ ├── SiteWrapper.js │ ├── Tag.js │ └── WidgetWrapper.js │ ├── hooks │ └── useSiteSetup.js │ ├── providers │ ├── SiteProvider.js │ └── WidgetProvider.js │ ├── styles │ ├── global.js │ ├── markdown.js │ ├── theme.js │ └── typography.js │ ├── templates │ ├── ReleaseTemplate.js │ └── ReleasesTemplate.js │ └── utils │ ├── constants.js │ ├── copyToClipboard.js │ ├── data.js │ └── windowPopup.js ├── widget ├── index.html ├── package.json ├── src │ ├── styles.css │ └── widget.js └── webpack.config.js └── yarn.lock /.github/main.workflow: -------------------------------------------------------------------------------- 1 | workflow "Build and Deploy ChangeCast" { 2 | resolves = [ 3 | "Alias Now Deployment", 4 | "Deploy with Netlify", 5 | ] 6 | on = "release" 7 | } 8 | 9 | action "Build" { 10 | uses = "./" 11 | secrets = ["GITHUB_TOKEN"] 12 | env = { 13 | DEPLOY_URL = "https://changecast-log.now.sh" 14 | } 15 | } 16 | 17 | action "Deploy with Netlify" { 18 | needs = "Build" 19 | uses = "netlify/actions/cli@master" 20 | args = "deploy --dir=./changecast --site=061cc43b-d700-492c-9e3d-3d92f6d197aa --prod" 21 | secrets = [ 22 | "NETLIFY_AUTH_TOKEN", 23 | ] 24 | } 25 | 26 | action "Deploy with Now" { 27 | uses = "actions/zeit-now@1.0.0" 28 | needs = ["Build"] 29 | args = "--public --no-clipboard --scope=palmer deploy ./changecast > $GITHUB_WORKSPACE/deploy.txt" 30 | secrets = ["ZEIT_TOKEN"] 31 | } 32 | 33 | action "Alias Now Deployment" { 34 | uses = "actions/zeit-now@1.0.0" 35 | args = "alias `cat $GITHUB_WORKSPACE/deploy.txt` changecast-log" 36 | secrets = [ 37 | "ZEIT_TOKEN", 38 | ] 39 | needs = ["Deploy with Now"] 40 | } 41 | 42 | workflow "Run Chronicler" { 43 | on = "pull_request" 44 | resolves = ["Chronicler"] 45 | } 46 | 47 | action "Chronicler" { 48 | uses = "crosscompile/chronicler-action@v1.0.1" 49 | secrets = ["GITHUB_TOKEN"] 50 | } 51 | 52 | workflow "Build and Deploy Docs Preview" { 53 | resolves = [ 54 | "Alias Material UI Preview", 55 | "Alias React Beautiful DnD Preview", 56 | "Alias Workbox Preview", 57 | "Alias Docs Preview", 58 | ] 59 | on = "pull_request" 60 | } 61 | 62 | action "Build React Beautiful DnD ChangeCast Preview" { 63 | uses = "./" 64 | secrets = ["GITHUB_TOKEN"] 65 | args = "DEPLOY_URL=https://changecast-1-$GITHUB_SHA.now.sh" 66 | env = { 67 | REPO_URL = "https://github.com/atlassian/react-beautiful-dnd" 68 | } 69 | } 70 | 71 | action "Deploy React Beautiful DnD Preview" { 72 | uses = "actions/zeit-now@1.0.0" 73 | args = "--public --no-clipboard --scope=palmer deploy ./changecast > $GITHUB_WORKSPACE/deploy.txt" 74 | secrets = ["ZEIT_TOKEN"] 75 | needs = ["Build React Beautiful DnD ChangeCast Preview"] 76 | } 77 | 78 | action "Alias React Beautiful DnD Preview" { 79 | uses = "actions/zeit-now@1.0.0" 80 | args = "alias --scope=palmer `cat $GITHUB_WORKSPACE/deploy.txt` changecast-1-$GITHUB_SHA" 81 | secrets = ["ZEIT_TOKEN"] 82 | needs = ["Deploy React Beautiful DnD Preview"] 83 | } 84 | 85 | action "Build Material UI ChangeCast Preview" { 86 | uses = "./" 87 | args = "DEPLOY_URL=https://changecast-2-$GITHUB_SHA.now.sh" 88 | secrets = ["GITHUB_TOKEN"] 89 | env = { 90 | REPO_URL = "https://github.com/mui-org/material-ui" 91 | } 92 | } 93 | 94 | action "Deploy Material UI Preview" { 95 | uses = "actions/zeit-now@1.0.0" 96 | secrets = ["ZEIT_TOKEN"] 97 | args = "--public --no-clipboard --scope=palmer deploy ./changecast > $GITHUB_WORKSPACE/deploy.txt" 98 | needs = ["Build Material UI ChangeCast Preview"] 99 | } 100 | 101 | action "Alias Material UI Preview" { 102 | uses = "actions/zeit-now@1.0.0" 103 | args = "alias --scope=palmer `cat $GITHUB_WORKSPACE/deploy.txt` changecast-2-$GITHUB_SHA" 104 | secrets = ["ZEIT_TOKEN"] 105 | needs = ["Deploy Material UI Preview"] 106 | } 107 | 108 | action "Build Workbox ChangeCast Preview" { 109 | uses = "./" 110 | args = "DEPLOY_URL=https://changecast-3-$GITHUB_SHA.now.sh" 111 | secrets = ["GITHUB_TOKEN"] 112 | env = { 113 | REPO_URL = "https://github.com/GoogleChrome/workbox" 114 | } 115 | } 116 | 117 | action "Deploy Workbox Preview" { 118 | uses = "actions/zeit-now@1.0.0" 119 | args = "--public --no-clipboard --scope=palmer deploy ./changecast > $GITHUB_WORKSPACE/deploy.txt" 120 | secrets = ["ZEIT_TOKEN"] 121 | needs = ["Build Workbox ChangeCast Preview"] 122 | } 123 | 124 | action "Alias Workbox Preview" { 125 | uses = "actions/zeit-now@1.0.0" 126 | secrets = ["ZEIT_TOKEN"] 127 | args = "alias --scope=palmer `cat $GITHUB_WORKSPACE/deploy.txt` changecast-3-$GITHUB_SHA" 128 | needs = ["Deploy Workbox Preview"] 129 | } 130 | 131 | action "Install and Build Docs Preview" { 132 | uses = "nuxt/actions-yarn@master" 133 | args = "install && FIRST_EXAMPLE_URL=https://changecast-1-$GITHUB_SHA.now.sh SECOND_EXAMPLE_URL=https://changecast-2-$GITHUB_SHA.now.sh THIRD_EXAMPLE_URL=https://changecast-3-$GITHUB_SHA.now.sh yarn build:docs" 134 | } 135 | 136 | action "Deploy Docs Preview" { 137 | uses = "actions/zeit-now@1.0.0" 138 | args = "--public --no-clipboard --scope=palmer deploy ./docs/public --local-config=../now.json > $GITHUB_WORKSPACE/deploy.txt" 139 | secrets = ["ZEIT_TOKEN"] 140 | needs = ["Install and Build Docs Preview"] 141 | } 142 | 143 | action "Alias Docs Preview" { 144 | uses = "actions/zeit-now@5c51b26db987d15a0133e4c760924896b4f1512f" 145 | secrets = ["ZEIT_TOKEN"] 146 | args = "alias --scope=palmer `cat $GITHUB_WORKSPACE/deploy.txt` changecast-$GITHUB_SHA" 147 | needs = ["Deploy Docs Preview"] 148 | } 149 | 150 | workflow "Build and Deploy Docs" { 151 | resolves = [ 152 | "Alias Material UI", 153 | "Alias Workbox", 154 | "Alias React Beautiful Dnd", 155 | "Alias Docs", 156 | ] 157 | on = "push" 158 | } 159 | 160 | action "Filter master" { 161 | uses = "actions/bin/filter@master" 162 | args = "branch master" 163 | } 164 | 165 | action "Build React Beautiful DnD ChangeCast" { 166 | uses = "./" 167 | secrets = ["GITHUB_TOKEN"] 168 | env = { 169 | REPO_URL = "https://github.com/atlassian/react-beautiful-dnd" 170 | DEPLOY_URL = "https://changecast-1.now.sh" 171 | } 172 | needs = ["Filter master"] 173 | } 174 | 175 | action "Deploy React Beautiful DnD" { 176 | uses = "actions/zeit-now@1.0.0" 177 | args = "--public --no-clipboard --scope=palmer deploy ./changecast > $GITHUB_WORKSPACE/deploy.txt" 178 | secrets = ["ZEIT_TOKEN"] 179 | needs = ["Build React Beautiful DnD ChangeCast"] 180 | } 181 | 182 | action "Alias React Beautiful Dnd" { 183 | uses = "actions/zeit-now@1.0.0" 184 | args = "alias --scope=palmer `cat $GITHUB_WORKSPACE/deploy.txt` changecast-1" 185 | secrets = ["ZEIT_TOKEN"] 186 | needs = ["Deploy React Beautiful DnD"] 187 | } 188 | 189 | action "Build Material UI ChangeCast" { 190 | uses = "./" 191 | args = "GITHUB_REPO_URL=" 192 | secrets = ["GITHUB_TOKEN"] 193 | env = { 194 | REPO_URL = "https://github.com/mui-org/material-ui" 195 | DEPLOY_URL = "https://changecast-2.now.sh" 196 | } 197 | needs = ["Filter master"] 198 | } 199 | 200 | action "Deploy Material UI" { 201 | uses = "actions/zeit-now@1.0.0" 202 | secrets = ["ZEIT_TOKEN"] 203 | args = "--public --no-clipboard --scope=palmer deploy ./changecast > $GITHUB_WORKSPACE/deploy.txt" 204 | needs = ["Build Material UI ChangeCast"] 205 | } 206 | 207 | action "Alias Material UI" { 208 | uses = "actions/zeit-now@1.0.0" 209 | args = "alias --scope=palmer `cat $GITHUB_WORKSPACE/deploy.txt` changecast-2" 210 | secrets = ["ZEIT_TOKEN"] 211 | needs = ["Deploy Material UI"] 212 | } 213 | 214 | action "Build Workbox ChangeCast" { 215 | uses = "./" 216 | secrets = ["GITHUB_TOKEN"] 217 | env = { 218 | REPO_URL = "https://github.com/GoogleChrome/workbox" 219 | DEPLOY_URL = "https://changecast-3.now.sh" 220 | } 221 | needs = ["Filter master"] 222 | } 223 | 224 | action "Deploy Workbox" { 225 | uses = "actions/zeit-now@1.0.0" 226 | args = "--public --no-clipboard --scope=palmer deploy ./changecast > $GITHUB_WORKSPACE/deploy.txt" 227 | secrets = ["ZEIT_TOKEN"] 228 | needs = ["Build Workbox ChangeCast"] 229 | } 230 | 231 | action "Alias Workbox" { 232 | uses = "actions/zeit-now@1.0.0" 233 | secrets = ["ZEIT_TOKEN"] 234 | args = "alias --scope=palmer `cat $GITHUB_WORKSPACE/deploy.txt` changecast-3" 235 | needs = ["Deploy Workbox"] 236 | } 237 | 238 | action "Install and Build Docs" { 239 | uses = "nuxt/actions-yarn@master" 240 | args = "install && FIRST_EXAMPLE_URL=https://changecast-1.now.sh SECOND_EXAMPLE_URL=https://changecast-2.now.sh THIRD_EXAMPLE_URL=https://changecast-3.now.sh yarn build:docs" 241 | needs = ["Filter master"] 242 | } 243 | 244 | action "Deploy Docs" { 245 | uses = "actions/zeit-now@1.0.0" 246 | args = "--public --no-clipboard --scope=palmer deploy ./docs/public --local-config=../now.json > $GITHUB_WORKSPACE/deploy.txt" 247 | secrets = ["ZEIT_TOKEN"] 248 | needs = ["Install and Build Docs"] 249 | } 250 | 251 | action "Alias Docs" { 252 | uses = "actions/zeit-now@5c51b26db987d15a0133e4c760924896b4f1512f" 253 | secrets = ["ZEIT_TOKEN"] 254 | args = "alias --scope=palmer `cat $GITHUB_WORKSPACE/deploy.txt` changecast" 255 | needs = ["Deploy Docs"] 256 | } 257 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variables file 55 | .env 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | static 61 | 62 | # Mac files 63 | .DS_Store 64 | 65 | # Yarn 66 | yarn-error.log 67 | .pnp/ 68 | .pnp.js 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | COPY . /changecast 4 | 5 | RUN cd /changecast && yarn 6 | 7 | ENTRYPOINT ["/changecast/action/entrypoint.sh"] 8 | 9 | LABEL "com.github.actions.name"="ChangeCast" 10 | LABEL "com.github.actions.description"="Create beautiful, performant, accessible changelogs from your Github releases." 11 | LABEL "com.github.actions.icon"="radio" 12 | LABEL "com.github.actions.color"="blue" 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present The Palmer Group 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |
6 |

7 | 8 | ChangeCast 9 | 10 |

11 |
12 | 13 | ## Introduction 14 | 15 | We built ChangeCast to help communicate project updates to users. Whether the project is an open source library or a paid website, users want to know about the hard work being done! Keeping a changelog [is important](https://keepachangelog.com), but the other half of the battle is making that changelog available to users. 16 | 17 | ChangeCast uses your [Github Releases](https://help.github.com/en/articles/creating-releases) to build a static site and widget that can be added to your application or project homepage. Check out the [examples on our homepage](https://changecast.now.sh) or get started deploying your own ChangeCast below. 18 | 19 | ## Getting Started 20 | 21 | ### Step One: Deploy 22 | 23 | #### Github Actions 24 | 25 | [Github Actions](https://github.com/features/actions) are the easiest way to deploy ChangeCast for your project. 26 | 27 |
28 | Instructions 29 | 30 | ##### 1. Add the ChangeCast Action 31 | 32 | ```HCL 33 | action "Build" { 34 | uses = "palmerhq/changecast@v1.0.0" 35 | env = { 36 | DEPLOY_URL = {DEPLOY_URL} 37 | } 38 | secrets = [ 39 | "GITHUB_TOKEN", 40 | ] 41 | } 42 | ``` 43 | 44 | Note that `URL` is necessary for SEO and Open Graph tags to work properly, but ChangeCast will build without it. You can skip this for your first deployment, and redeploy once you know the deployment URL. 45 | 46 | ##### 2. Add a static deployment Action 47 | 48 | In the example below we are using [Netlify](https://www.netlify.com), but any static deployment action should work. Simply configure the action to deploy the `./changecast` directory that is created by the ChangeCast Action. 49 | 50 | ```HCL 51 | action "Publish with Netlify" { 52 | needs = "Build" 53 | uses = "netlify/actions/cli@master" 54 | args = "deploy --dir=./changecast --prod" 55 | secrets = [ 56 | "NETLIFY_AUTH_TOKEN", 57 | "NETLIFY_SITE_ID", 58 | ] 59 | } 60 | ``` 61 | 62 | Note that you can generate a new `NETLIFY_SITE_ID` by installing the [Netlify CLI](https://github.com/netlify/cli) and running `netlify sites:create`. 63 | 64 | As a bonus you can also try the [Chronicler Action](https://github.com/marketplace/actions/chronicler-action) to help you draft release notes from PR titles. 65 | 66 | For a full working example of deploying ChangeCast using Github Actions, check out our [main.workflow](https://github.com/palmerhq/changecast/blob/master/.github/main.workflow). 67 | 68 |
69 | 70 | #### Netlify 71 | 72 | [Netlify](https://www.netlify.com) is the next easiest way to deploy ChangeCast for your project. 73 | 74 |
75 | Instructions 76 | 77 | ##### 1. Deploy 78 | 79 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/palmerhq/changecast) 80 | 81 | You will be prompted for the following information: 82 | 83 | - **Github repository url**: Enter the url of a Github repository _(e.g. https://github.com/facebook/react)_. 84 | - **Github access token (optional for public repos)**: [Generate](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line) and enter an access token with `repo` scope. 85 | 86 | After deploying, you can assign a custom domain for your changelog [using Netlify](https://www.netlify.com/docs/custom-domains/). 87 | 88 | ##### 2. Add a Build Trigger 89 | 90 | In order to rebuild whenever a Github release is published, we want to add a webhook for Github releases to Netlify. The steps to do so are: 91 | 92 | 1. In your Netlify site's "Build & deploy" settings, find the "Github Releases" build hook and copy the URL displayed. 93 | 2. Create a webhook in the Github repository (https://github.com/{owner}/{name}/settings/hooks/new). 94 | 3. Paste the URL from step 1 into the Github webhook's "Payload URL". 95 | 4. In the Github webhook under "Which events would you like to trigger this webhook?", select "Let me select individual events." and "Releases". 96 | 97 | You're all set! Now your changelog page and widget will rebuild whenever a new release is published. 98 | 99 |
100 | 101 | ### Step Two: Embed Widget 102 | 103 | First, add the following `script` tag to your site header. `DEPLOY_URL` should be the deployment URL of your ChangeCast site. 104 | 105 | ```html 106 | 107 | ``` 108 | 109 | Next, add the `data-toggle-changecast` attribute to any clickable elements that you want to toggle the ChangeCast widget. 110 | 111 | ```html 112 | 113 | ``` 114 | -------------------------------------------------------------------------------- /action/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -l 2 | 3 | cd /changecast 4 | 5 | if [ -z ${REPO_URL+x} ]; then 6 | sh -c "REPO_URL=https://github.com/$GITHUB_REPOSITORY $* yarn build" 7 | else 8 | sh -c "REPO_URL=$REPO_URL $* yarn build" 9 | fi 10 | 11 | mkdir "$GITHUB_WORKSPACE/changecast" 12 | cp -r /changecast/site/public/. "$GITHUB_WORKSPACE/changecast" 13 | -------------------------------------------------------------------------------- /docs/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | exports.onClientEntry = () => { 2 | const oneMonthAgo = new Date() 3 | oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1) 4 | const oneMonthAgoISO = oneMonthAgo.toISOString() 5 | 6 | window.localStorage.setItem('changecast-077d7', oneMonthAgoISO) 7 | window.localStorage.setItem('changecast-2c277', oneMonthAgoISO) 8 | window.localStorage.setItem('changecast-ff975', oneMonthAgoISO) 9 | } 10 | -------------------------------------------------------------------------------- /docs/gatsby-config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { config } = require('dotenv') 3 | 4 | config({ path: path.resolve('..', '.env') }) 5 | 6 | module.exports = { 7 | siteMetadata: { 8 | exampleSiteUrls: [ 9 | process.env.FIRST_EXAMPLE_URL, 10 | process.env.SECOND_EXAMPLE_URL, 11 | process.env.THIRD_EXAMPLE_URL, 12 | ], 13 | }, 14 | plugins: [ 15 | 'gatsby-plugin-emotion', 16 | 'gatsby-plugin-react-helmet', 17 | { 18 | resolve: 'gatsby-transformer-og-image', 19 | options: { 20 | fontPath: '../fonts/Inter-SemiBold.woff', 21 | fontColor: '#24292e', 22 | backgroundColor: '#f7f7f7', 23 | }, 24 | }, 25 | 'gatsby-transformer-favicons', 26 | { 27 | resolve: `gatsby-plugin-typography`, 28 | options: { 29 | pathToConfigModule: `src/styles/typography`, 30 | }, 31 | }, 32 | `gatsby-transformer-sharp`, 33 | `gatsby-plugin-sharp`, 34 | { 35 | resolve: `gatsby-source-filesystem`, 36 | options: { 37 | name: `images`, 38 | path: path.join(__dirname, `src`, `images`), 39 | }, 40 | }, 41 | { 42 | resolve: `gatsby-plugin-google-analytics`, 43 | options: { 44 | trackingId: 'UA-139165006-1', 45 | }, 46 | }, 47 | ], 48 | } 49 | -------------------------------------------------------------------------------- /docs/gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | exports.createPages = async ({ graphql, actions: { createPage } }) => { 4 | const indexTemplate = path.resolve('./src/templates/IndexTemplate.js') 5 | const ogText = 'ChangeCast' 6 | 7 | createPage({ 8 | path: `/`, 9 | component: indexTemplate, 10 | context: { 11 | ogText, 12 | }, 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /docs/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "changecast-docs", 4 | "builds": [ 5 | { 6 | "use": "@now/static" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "changecast-docs", 3 | "private": true, 4 | "description": "ChangeCast documentation and landing page.", 5 | "version": "0.1.0", 6 | "author": "Jack Cross ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "gatsby build", 10 | "develop": "gatsby develop", 11 | "serve": "gatsby serve", 12 | "now-build": "yarn build" 13 | }, 14 | "dependencies": { 15 | "@emotion/core": "^10.0.10", 16 | "gatsby": "^2.1.22", 17 | "gatsby-image": "^2.0.37", 18 | "gatsby-plugin-emotion": "^4.0.4", 19 | "gatsby-plugin-google-analytics": "^2.0.18", 20 | "gatsby-plugin-react-helmet": "^3.0.7", 21 | "gatsby-plugin-sharp": "^2.0.32", 22 | "gatsby-plugin-typography": "^2.2.10", 23 | "gatsby-source-filesystem": "^2.0.23", 24 | "gatsby-transformer-favicons": "file:../plugins/gatsby-transformer-favicons", 25 | "gatsby-transformer-og-image": "file:../plugins/gatsby-transformer-og-image", 26 | "gatsby-transformer-sharp": "^2.1.17", 27 | "icons": "0.1.0", 28 | "normalize.css": "^8.0.1", 29 | "react": "^16.8.1", 30 | "react-dom": "^16.8.1", 31 | "react-helmet": "^5.2.0", 32 | "react-typography": "^0.16.19", 33 | "typography": "^0.16.19" 34 | }, 35 | "devDependencies": {} 36 | } 37 | -------------------------------------------------------------------------------- /docs/src/components/Anchor.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Anchor = ({ children, ...props }) => ( 4 | 8 | {children} 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /docs/src/components/Button/AnchorButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { buttonStyles } from './Button' 3 | 4 | export const AnchorButton = props => 5 | -------------------------------------------------------------------------------- /docs/src/components/Button/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const buttonStyles = { 4 | color: 'white', 5 | display: 'inline-block', 6 | verticalAlign: 'bottom', 7 | position: 'relative', 8 | padding: '12px 20px 12px', 9 | border: '2px solid transparent', 10 | borderRadius: '4px', 11 | fontWeight: '700', 12 | userSelect: 'none', 13 | textAlign: 'center', 14 | textDecoration: 'none', 15 | cursor: 'pointer', 16 | whiteSpace: 'nowrap', 17 | transition: 'background 200ms ease, transform 200ms ease', 18 | ':hover': { 19 | background: 'rgba(0, 0, 0, 0.1)', 20 | }, 21 | } 22 | 23 | export const Button = props => 54 | 55 |
  • 61 | 68 | 69 | 76 | ChangeCast 77 | 78 | 79 |
  • 80 | 81 | 82 | 83 | 84 | ) 85 | -------------------------------------------------------------------------------- /docs/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import { graphql, useStaticQuery } from 'gatsby' 2 | import Img from 'gatsby-image' 3 | import { Radio } from 'icons/Radio' 4 | import React from 'react' 5 | import { AnchorButton } from './Button/AnchorButton' 6 | import { Button } from './Button/Button' 7 | import { LinkButton } from './Button/LinkButton' 8 | 9 | export const Header = () => { 10 | const { 11 | file: { 12 | childImageSharp: { fluid }, 13 | }, 14 | } = useStaticQuery(graphql` 15 | query { 16 | file(relativePath: { eq: "oleg-laptev-546607-unsplash.png" }) { 17 | childImageSharp { 18 | fluid(maxWidth: 600) { 19 | ...GatsbyImageSharpFluid_tracedSVG 20 | } 21 | } 22 | } 23 | } 24 | `) 25 | 26 | return ( 27 | <> 28 |
    34 | 124 |
    141 |
    151 |

    160 | Keep users informed. 161 |

    162 |

    163 | Create{' '} 164 | 165 | beautiful 166 | 167 | ,{' '} 168 | 169 | performant 170 | 171 | ,{' '} 172 | 173 | accessible 174 | {' '} 175 | changelogs from your Github releases. 176 |

    177 |
    178 |
    191 | 200 |
    201 |
    202 |
    203 | 204 | ) 205 | } 206 | -------------------------------------------------------------------------------- /docs/src/components/Link.js: -------------------------------------------------------------------------------- 1 | import { Link as GatsbyLink } from 'gatsby' 2 | import isAbsoluteURL from 'is-absolute-url' 3 | import React from 'react' 4 | 5 | export const Link = ({ href, ...props }) => 6 | isAbsoluteURL(href || '') ? ( 7 |
    8 | ) : ( 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /docs/src/components/Section.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Section = ({ background, children }) => ( 4 |
    {children}
    5 | ) 6 | -------------------------------------------------------------------------------- /docs/src/images/ChangeCast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmerhq/changecast/56c5af29bf38fde8433b19d5a6cf30f2354a6709/docs/src/images/ChangeCast.png -------------------------------------------------------------------------------- /docs/src/images/ChangeCastTransparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmerhq/changecast/56c5af29bf38fde8433b19d5a6cf30f2354a6709/docs/src/images/ChangeCastTransparent.png -------------------------------------------------------------------------------- /docs/src/images/oleg-laptev-546607-unsplash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmerhq/changecast/56c5af29bf38fde8433b19d5a6cf30f2354a6709/docs/src/images/oleg-laptev-546607-unsplash.png -------------------------------------------------------------------------------- /docs/src/styles/global.js: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/core' 2 | import 'normalize.css' 3 | import { fonts } from './typography' 4 | 5 | export const globalStyles = css` 6 | * { 7 | box-sizing: border-box; 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | html { 13 | height: 100%; 14 | font-family: ${fonts.regular}, sans-serif; 15 | font-style: normal; 16 | line-height: 1.15; 17 | text-rendering: optimizeLegibility; 18 | -webkit-text-size-adjust: 100%; 19 | -ms-text-size-adjust: 100%; 20 | -ms-overflow-style: scrollbar; 21 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 22 | } 23 | 24 | *::-moz-selection, 25 | *::-moz-selection { 26 | color: white; 27 | background-color: #4d61fc; 28 | } 29 | 30 | *::-moz-selection, 31 | *::selection { 32 | color: white; 33 | background-color: #4d61fc; 34 | } 35 | 36 | body { 37 | background-color: white; 38 | color: #3d3d3d; 39 | } 40 | body.state-fixed-body { 41 | overflow: hidden; 42 | } 43 | 44 | p { 45 | margin-bottom: 1em; 46 | margin-top: 1em; 47 | line-height: 1.4; 48 | } 49 | 50 | hr { 51 | box-sizing: content-box; 52 | height: 0; 53 | overflow: visible; 54 | } 55 | 56 | input, 57 | button, 58 | select, 59 | optgroup, 60 | textarea { 61 | margin: 0; 62 | } 63 | 64 | button, 65 | input { 66 | border: none; 67 | background: none; 68 | overflow: visible; 69 | } 70 | 71 | button { 72 | border-radius: 0; 73 | } 74 | 75 | h1, 76 | h2, 77 | h3, 78 | h4, 79 | h5, 80 | h6 { 81 | margin-bottom: 0.5em; 82 | margin-top: 0.5em; 83 | line-height: 1.3; 84 | color: #303030; 85 | } 86 | ` 87 | -------------------------------------------------------------------------------- /docs/src/styles/typography.js: -------------------------------------------------------------------------------- 1 | import Typography from 'typography' 2 | import '../../../fonts/fonts.css' 3 | 4 | export const fonts = { 5 | regular: 'Inter Regular', 6 | regularItalic: 'Inter Italic', 7 | semibold: 'Inter Semibold', 8 | semiboldItalic: 'Inter Semibold Italic', 9 | bold: 'Inter Bold', 10 | boldItalic: 'Inter Bold Italic', 11 | } 12 | 13 | const typography = new Typography({ 14 | baseFontSize: '20px', 15 | baseLineHeight: 1.55, 16 | headerLineHeight: 1.4, 17 | headerFontFamily: [fonts.bold, 'sans-serif'], 18 | bodyFontFamily: [fonts.regular, 'sans-serif'], 19 | // headerColor: 'hsla(0,0%,0%,0.9)', 20 | // bodyColor: 'hsla(0,0%,0%,0.8)', 21 | 22 | overrideStyles: ({ rhythm }) => ({ 23 | h1: { 24 | // color: 'hsla(0,0%,0%,0.75)', 25 | }, 26 | h2: { 27 | // color: 'hsla(0,0%,0%,0.775)', 28 | }, 29 | h3: { 30 | // color: 'hsla(0,0%,0%,0.8)', 31 | }, 32 | 'h1,h2,h3,h4,h5,h6': { 33 | lineHeight: 1, 34 | }, 35 | 'h1,h2,h3,h4': { 36 | lineHeight: 1.25, 37 | marginTop: rhythm(1), 38 | marginBottom: rhythm(1 / 2), 39 | }, 40 | }), 41 | }) 42 | // Hot reload typography in development. 43 | if (process.env.NODE_ENV !== 'production') { 44 | typography.injectStyles() 45 | } 46 | 47 | export default typography 48 | export const rhythm = typography.rhythm 49 | export const scale = typography.scale 50 | -------------------------------------------------------------------------------- /docs/src/templates/IndexTemplate.js: -------------------------------------------------------------------------------- 1 | import { Global } from '@emotion/core' 2 | import { graphql } from 'gatsby' 3 | import React from 'react' 4 | import Helmet from 'react-helmet' 5 | import { Anchor } from '../components/Anchor' 6 | import { ButtonContainer } from '../components/Button/ButtonContainer' 7 | import { GlowingAnchorButton } from '../components/Button/GlowingAnchorButton' 8 | import { GlowingButton } from '../components/Button/GlowingButton' 9 | import { CenteredText } from '../components/CenteredText' 10 | import { CenteredTitle } from '../components/CenteredTitle' 11 | import { Favicons } from '../components/Favicons' 12 | import { Feature } from '../components/Feature' 13 | import { Features } from '../components/Features' 14 | import { FocusStyles } from '../components/FocusStyles' 15 | import { Footer } from '../components/Footer' 16 | import { Header } from '../components/Header' 17 | import { Section } from '../components/Section' 18 | import { globalStyles } from '../styles/global' 19 | 20 | const url = 'https://changecast.now.sh' 21 | const title = 'ChangeCast' 22 | const description = 23 | 'Create beautiful, performant, accessible changelogs from your Github releases.' 24 | 25 | const IndexTemplate = ({ 26 | data: { 27 | site: { 28 | siteMetadata: { 29 | exampleSiteUrls: [reactBeautifulDndUrl, materialUiUrl, workboxUrl], 30 | }, 31 | }, 32 | logo: { 33 | childFavicon: { faviconElements }, 34 | childOgImage: { 35 | ogImageWithText: { src: ogImgSrc }, 36 | }, 37 | }, 38 | }, 39 | }) => ( 40 | <> 41 | 42 | 43 | 44 | 68 | {(process.env.NODE_ENV === 'development' || 69 | typeof window === 'undefined') && ( 70 | 71 | 11 | 12 | 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /widget/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "changecast-widget", 3 | "private": true, 4 | "description": "Embedded ChangeCast widget.", 5 | "version": "0.1.0", 6 | "author": "Jack Cross ", 7 | "license": "MIT", 8 | "scripts": { 9 | "develop": "webpack-dev-server --open --mode=development", 10 | "build": "webpack --mode=production" 11 | }, 12 | "devDependencies": { 13 | "@babel/core": "^7.3.3", 14 | "@babel/preset-env": "^7.3.1", 15 | "babel-loader": "^8.0.5", 16 | "concurrently": "^4.1.0", 17 | "copy-webpack-plugin": "^4.6.0", 18 | "css-loader": "^2.1.0", 19 | "dotenv": "^6.2.0", 20 | "style-loader": "^0.23.1", 21 | "webpack": "^4.29.5", 22 | "webpack-cli": "^3.2.3", 23 | "webpack-dev-server": "^3.1.14" 24 | }, 25 | "dependencies": { 26 | "focus-trap": "^4.0.2", 27 | "whatwg-fetch": "^3.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /widget/src/styles.css: -------------------------------------------------------------------------------- 1 | .notification { 2 | display: inline-block; 3 | border-radius: 50%; 4 | width: 22px; 5 | height: 22px; 6 | background-color: #ff3e43; 7 | position: absolute; 8 | right: -10px; 9 | top: -10px; 10 | color: #f7f7f7; 11 | font-size: 11px; 12 | text-align: center; 13 | line-height: 22px; 14 | font-weight: bold; 15 | opacity: 1; 16 | letter-spacing: 0; 17 | animation: growIn 500ms cubic-bezier(0.175, 0.985, 0.1, 1.035); 18 | } 19 | 20 | .iframe { 21 | opacity: 1; 22 | position: fixed; 23 | height: 100vh; 24 | width: 100vw; 25 | top: 0; 26 | right: 0; 27 | margin: 0; 28 | padding: 0; 29 | border: none; 30 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); 31 | animation: slideOut 500ms cubic-bezier(0.175, 0.985, 0.1, 1.035); 32 | } 33 | 34 | .iframeOpen { 35 | transform: translateX(0); 36 | animation: slideIn 500ms cubic-bezier(0.175, 0.985, 0.1, 1.035); 37 | } 38 | 39 | .iframeHidden { 40 | opacity: 0; 41 | pointer-events: none; 42 | } 43 | 44 | .overlay { 45 | height: 100%; 46 | width: 100%; 47 | background: black; 48 | opacity: 0; 49 | top: 0; 50 | left: 0; 51 | position: fixed; 52 | transition: opacity 350ms linear; 53 | } 54 | 55 | .overlayOpen { 56 | opacity: 0.5; 57 | visibility: visible; 58 | } 59 | 60 | .overlayHidden { 61 | visibility: hidden; 62 | } 63 | 64 | @media (min-width: 800px) { 65 | .iframe { 66 | width: 399px; 67 | } 68 | } 69 | 70 | @keyframes slideIn { 71 | from { 72 | transform: translateX(100%); 73 | } 74 | to { 75 | transform: translateX(0); 76 | } 77 | } 78 | 79 | @keyframes slideOut { 80 | from { 81 | transform: translateX(0); 82 | } 83 | to { 84 | transform: translateX(100%); 85 | } 86 | } 87 | 88 | @keyframes growIn { 89 | from { 90 | transform: scale(0.1); 91 | } 92 | to { 93 | transform: scale(1); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /widget/src/widget.js: -------------------------------------------------------------------------------- 1 | import createFocusTrap from 'focus-trap' 2 | import { fetch } from 'whatwg-fetch' 3 | import * as styles from './styles.css' 4 | 5 | // configuration 6 | const CHANGECAST_LOCALSTORAGE_KEY = `changecast-${process.env.REPO_HASH}` 7 | const changeCastHost = 8 | process.env.DEPLOY_URL || 9 | process.env.URL || 10 | document.currentScript.getAttribute('src').replace('/widget.js', '') 11 | 12 | // find all toggles 13 | const toggleSelectors = 14 | document.currentScript.getAttribute('data-selectors') || 15 | '[data-toggle-changecast]' 16 | 17 | const toggles = document.querySelectorAll(toggleSelectors) 18 | 19 | function createWidget() { 20 | // bail early if the widget has already been created 21 | if (document.querySelector(styles.iframe)) { 22 | return 23 | } 24 | 25 | // add click handlers to toggles 26 | toggles.forEach(toggle => toggle.addEventListener('click', toggleChangeCast)) 27 | 28 | // create overlay 29 | const overlay = document.createElement('div') 30 | 31 | // create iframe 32 | const iframe = document.createElement('iframe') 33 | iframe.src = `${changeCastHost}/widget` 34 | iframe.allowFullscreen = true 35 | iframe.scrolling = 'no' 36 | iframe.tabIndex = 0 37 | iframe.setAttribute('role', 'dialog') 38 | iframe.setAttribute('aria-label', 'ChangeCast Changelog') 39 | iframe.setAttribute('aria-hidden', true) 40 | iframe.setAttribute('tabindex', -1) 41 | 42 | // hide overlay and iframe to start 43 | overlay.className = `${styles.overlay} ${styles.overlayHidden}` 44 | iframe.className = `${styles.iframe} ${styles.iframeHidden}` 45 | 46 | document.body.appendChild(overlay) 47 | document.body.appendChild(iframe) 48 | 49 | let focusTrap = createFocusTrap(iframe, { 50 | initialFocus: iframe, 51 | }) 52 | 53 | // shared state 54 | let open = false 55 | let toggleNotifications = new Map() 56 | 57 | function openChangeCast() { 58 | open = true 59 | 60 | iframe.contentWindow.postMessage('open', '*') 61 | 62 | overlay.className = `${styles.overlay} ${styles.overlayOpen}` 63 | iframe.className = `${styles.iframe} ${styles.iframeOpen}` 64 | iframe.setAttribute('aria-hidden', false) 65 | iframe.removeAttribute('tabindex') 66 | 67 | focusTrap.activate() 68 | window.addEventListener('click', toggleChangeCast, true) 69 | 70 | window.localStorage.setItem( 71 | CHANGECAST_LOCALSTORAGE_KEY, 72 | new Date().toISOString() 73 | ) 74 | 75 | if (toggleNotifications.size) { 76 | toggles.forEach(toggle => { 77 | toggle.removeChild(toggleNotifications.get(toggle)) 78 | toggleNotifications.delete(toggle) 79 | }) 80 | } 81 | } 82 | 83 | function closeChangeCast() { 84 | open = false 85 | 86 | focusTrap.deactivate() 87 | window.removeEventListener('click', toggleChangeCast, true) 88 | 89 | overlay.className = styles.overlay 90 | iframe.className = styles.iframe 91 | iframe.setAttribute('aria-hidden', true) 92 | iframe.setAttribute('tabindex', -1) 93 | 94 | setTimeout(() => { 95 | overlay.className = `${styles.overlay} ${styles.overlayHidden}` 96 | iframe.className = `${styles.iframe} ${styles.iframeHidden}` 97 | iframe.contentWindow.postMessage('close', '*') 98 | }, 400) 99 | } 100 | 101 | function toggleChangeCast() { 102 | if (open) { 103 | closeChangeCast() 104 | } else { 105 | openChangeCast() 106 | } 107 | } 108 | 109 | // listen for close events from the iframe 110 | window.addEventListener( 111 | 'message', 112 | event => { 113 | if (event.origin === changeCastHost) { 114 | closeChangeCast() 115 | } 116 | }, 117 | true 118 | ) 119 | 120 | // notifications 121 | const notification = document.createElement('span') 122 | notification.setAttribute('data-changecast-notification', true) 123 | notification.className = styles.notification 124 | 125 | const toggleStyle = document.createElement('style') 126 | document.head.appendChild(toggleStyle) 127 | toggleStyle.sheet.insertRule(`${toggleSelectors} { position: relative; }`) 128 | 129 | fetch(`${changeCastHost}/release-dates.json`) 130 | .then( 131 | res => res.json(), 132 | err => { 133 | // swallow error 134 | } 135 | ) 136 | .then(dates => { 137 | const lastViewed = window.localStorage.getItem( 138 | CHANGECAST_LOCALSTORAGE_KEY 139 | ) 140 | 141 | if (lastViewed) { 142 | const lastViewedDate = new Date(lastViewed) 143 | const lastViewedIndex = dates.findIndex( 144 | date => new Date(date) <= lastViewedDate 145 | ) 146 | const count = lastViewedIndex === -1 ? dates.length : lastViewedIndex 147 | 148 | if (count > 0) { 149 | notification.innerHTML = count > 9 ? `9+` : count 150 | toggles.forEach(toggle => { 151 | const notificationCopy = notification.cloneNode(true) 152 | toggleNotifications.set(toggle, notificationCopy) 153 | toggle.appendChild(notificationCopy) 154 | }) 155 | } 156 | } else { 157 | window.localStorage.setItem( 158 | CHANGECAST_LOCALSTORAGE_KEY, 159 | new Date().toISOString() 160 | ) 161 | } 162 | }) 163 | } 164 | 165 | window.addEventListener('load', createWidget) 166 | -------------------------------------------------------------------------------- /widget/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const crypto = require('crypto') 3 | const webpack = require('webpack') 4 | const copyWebpackPlugin = require('copy-webpack-plugin') 5 | const { config } = require('dotenv') 6 | 7 | config({ path: path.resolve('..', '.env') }) 8 | 9 | const bundleOutputDir = '../site/static' 10 | 11 | const repoHash = crypto 12 | .createHash(`md5`) 13 | .update(process.env.REPO_URL) 14 | .digest(`hex`) 15 | 16 | const shortRepoHash = repoHash.substr(repoHash.length - 5) 17 | 18 | module.exports = (env, { mode }) => { 19 | return [ 20 | { 21 | entry: './src/widget.js', 22 | output: { 23 | filename: 'widget.js', 24 | path: path.resolve(bundleOutputDir), 25 | }, 26 | devServer: { 27 | contentBase: bundleOutputDir, 28 | }, 29 | optimization: { 30 | minimize: mode === 'production', 31 | }, 32 | plugins: [ 33 | new webpack.EnvironmentPlugin({ 34 | URL: process.env.DEPLOY_URL || process.env.URL || '', 35 | REPO_HASH: shortRepoHash, 36 | }), 37 | ...(mode === 'development' 38 | ? [ 39 | new webpack.SourceMapDevToolPlugin(), 40 | new copyWebpackPlugin([{ from: './' }]), 41 | ] 42 | : []), 43 | ], 44 | module: { 45 | rules: [ 46 | { 47 | test: /\.css$/, 48 | use: [ 49 | 'style-loader', 50 | { 51 | loader: 'css-loader', 52 | options: { 53 | modules: true, 54 | hashPrefix: shortRepoHash, 55 | }, 56 | }, 57 | ], 58 | }, 59 | { 60 | test: /\.js$/, 61 | exclude: /node_modules/, 62 | use: { 63 | loader: 'babel-loader', 64 | options: { 65 | presets: [ 66 | [ 67 | '@babel/env', 68 | { 69 | targets: { 70 | browsers: ['ie 6', 'safari 7'], 71 | }, 72 | }, 73 | ], 74 | ], 75 | }, 76 | }, 77 | }, 78 | ], 79 | }, 80 | }, 81 | ] 82 | } 83 | --------------------------------------------------------------------------------