Bunch of website stuff
This commit is contained in:
parent
7e29e21483
commit
87524d4947
Binary file not shown.
After ![]() (image error) Size: 36 KiB |
|
@ -0,0 +1,13 @@
|
|||
import {
|
||||
continents as ContinentsIndex,
|
||||
countries as CountriesIndex
|
||||
} from 'countries-list'
|
||||
|
||||
const getCountriesByContinent = (continent) =>
|
||||
Object.keys(CountriesIndex).filter(country => CountriesIndex[country].continent === continent)
|
||||
|
||||
export {
|
||||
ContinentsIndex,
|
||||
CountriesIndex,
|
||||
getCountriesByContinent
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
export const addEvent = (node, type, handler) => {
|
||||
if (node.addEventListener) {
|
||||
node.addEventListener(type, handler, false)
|
||||
} else if (node.attachEvent) {
|
||||
node.attachEvent('on' + type, handler)
|
||||
}
|
||||
}
|
||||
|
||||
export const removeEvent = (node, type, handler) => {
|
||||
if (node.removeEventListener) {
|
||||
node.removeEventListener(type, handler, false)
|
||||
} else if (node.detachEvent) {
|
||||
node.detachEvent('on' + type, handler)
|
||||
}
|
||||
}
|
Binary file not shown.
After ![]() (image error) Size: 13 KiB |
|
@ -0,0 +1,15 @@
|
|||
export const formatNumber = (n) => {
|
||||
const digits = String(n).split('')
|
||||
const groups = []
|
||||
|
||||
while (digits.length)
|
||||
groups.unshift(digits.splice(-3).join(''))
|
||||
|
||||
return groups.join(',')
|
||||
}
|
||||
|
||||
export const parseNumber = (s) =>
|
||||
parseInt(s.replace(/,/g, ''), 10)
|
||||
|
||||
export const formatPercent = (n, fixed = 1) =>
|
||||
String((n.toPrecision(2) * 100).toFixed(fixed))
|
Binary file not shown.
After ![]() (image error) Size: 27 KiB |
|
@ -0,0 +1,9 @@
|
|||
import React from 'react'
|
||||
import contentHTML from './About.md'
|
||||
|
||||
class About extends React.Component {
|
||||
render = () =>
|
||||
<div className="wrapper" dangerouslySetInnerHTML={{ __html: contentHTML }}/>
|
||||
}
|
||||
|
||||
export default About
|
|
@ -0,0 +1,46 @@
|
|||
npmcdn is an [open source](https://github.com/mjackson/npmcdn) project built by me, [Michael Jackson](https://twitter.com/mjackson). I built it because, as an npm package author, it felt tedious for me to use existing, git-based CDNs to make my open source work available via CDN. Development was sponsored by my company, [React Training](https://reactjs-training.com).
|
||||
|
||||
<div class="sponsor-logos">
|
||||
<div class="sponsor-logo">
|
||||
<a href="https://reactjs-training.com"><img src="../ReactTrainingLogo.png"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
We'd love to talk to you more about training your team on [React](https://facebook.github.io/react/). Please [get in touch](mailto:hello@reactjs-training.com) if interested.
|
||||
|
||||
### Cache Behavior
|
||||
|
||||
The CDN caches all files based on their permanent URL, which includes the npm package version. This works because npm does not allow package authors to overwrite a package that has already been published with a different one at the same version number.
|
||||
|
||||
URLs that do not specify a package version number redirect to one that does. This is the `latest` version when no version is specified, or the `maxSatisfying` version when a [semver version](https://github.com/npm/node-semver) is given. Redirects are cached for 5 minutes.
|
||||
|
||||
Browsers are instructed (via the `Cache-Control` header) to cache assets for 4 hours.
|
||||
|
||||
### Sponsors
|
||||
|
||||
The fast, global infrastructure that powers npmcdn is graciously provided by [CloudFlare](https://www.cloudflare.com) and [Heroku](https://www.heroku.com).
|
||||
|
||||
<div class="sponsor-logos">
|
||||
<div class="sponsor-logo">
|
||||
<a href="https://www.cloudflare.com"><img src="../CloudFlareLogo.png"></a>
|
||||
</div>
|
||||
<div class="sponsor-logo">
|
||||
<a href="https://www.heroku.com"><img src="../HerokuLogo.png"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
These sponsors provide some of the most robust, reliable infrastructure available today and I'm happy to be able to partner with them on npmcdn.
|
||||
|
||||
### Support
|
||||
|
||||
npmcdn is a free, best-effort service and cannot provide any uptime or support guarantees.
|
||||
|
||||
I do my best to keep it running, but sometimes things go wrong. Sometimes there are network or provider issues outside my control. Sometimes abusive traffic temporarily affects response times. Sometimes I break things by doing something dumb, but I try not to.
|
||||
|
||||
The goal of npmcdn is to provide a hassle-free CDN for npm package authors. It's also a great resource for people creating demos and instructional material. However, if you rely on it to serve files that are crucial to your business, you should probably pay for a host with well-supported infrastructure and uptime guarantees.
|
||||
|
||||
npmcdn is not affiliated with or supported by npm, Inc. in any way. Please do not contact npm for help with npmcdn.
|
||||
|
||||
### Feedback
|
||||
|
||||
If you think this is useful, I'd love to hear from you. Please reach out to [@mjackson](https://twitter.com/mjackson) with any questions/concerns.
|
|
@ -1,36 +0,0 @@
|
|||
body {
|
||||
font: 14px Helvetica, sans-serif;
|
||||
line-height: 1.5;
|
||||
padding: 5px 20px;
|
||||
}
|
||||
@media all and (min-width: 660px) {
|
||||
body {
|
||||
padding: 50px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
header, #wrapper {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
table {
|
||||
border-color: black;
|
||||
border-style: solid;
|
||||
border-width: 0 0 1px 1px;
|
||||
}
|
||||
table th, table td {
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
padding: 5px 7px;
|
||||
border-color: black;
|
||||
border-style: solid;
|
||||
border-width: 1px 1px 0 0;
|
||||
}
|
|
@ -1,86 +1,9 @@
|
|||
import React from 'react'
|
||||
import './Home.css'
|
||||
import contentHTML from './Home.md'
|
||||
|
||||
const Home = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<header>
|
||||
<h1>npmcdn</h1>
|
||||
</header>
|
||||
<section id="wrapper">
|
||||
<p>npmcdn is a CDN for packages that are published via <a href="https://www.npmjs.com/">npm</a>. Use it to quickly and easily load files using a simple URL like <code>https://npmcdn.com/package@version/path/to/file</code>.</p>
|
||||
<p>A few examples:</p>
|
||||
<ul>
|
||||
<li><a href="/react@15.0.1/dist/react.min.js">https://npmcdn.com/react@15.0.1/dist/react.min.js</a></li>
|
||||
<li><a href="/react-dom@15.0.1/dist/react-dom.min.js">https://npmcdn.com/react-dom@15.0.1/dist/react-dom.min.js</a></li>
|
||||
<li><a href="/history@1.12.5/umd/History.min.js">https://npmcdn.com/history@1.12.5/umd/History.min.js</a></li>
|
||||
<li><a href="/react-router@1.0.0/umd/ReactRouter.min.js">https://npmcdn.com/react-router@1.0.0/umd/ReactRouter.min.js</a></li>
|
||||
</ul>
|
||||
|
||||
<p>You may also use a <a href="https://docs.npmjs.com/cli/dist-tag">tag</a> or <a href="https://docs.npmjs.com/misc/semver">version range</a> instead of a fixed version number, or omit the version/tag entirely to use the <code>latest</code> tag.</p>
|
||||
<ul>
|
||||
<li><a href="/react@^0.14/dist/react.min.js">https://npmcdn.com/react@^0.14/dist/react.min.js</a></li>
|
||||
<li><a href="/react/dist/react.min.js">https://npmcdn.com/react/dist/react.min.js</a></li>
|
||||
</ul>
|
||||
|
||||
<p>If you omit the file path, the <a href="https://docs.npmjs.com/files/package.json#main">main module</a> will be served. This is especially useful for loading libaries that publish a UMD build as their main module.</p>
|
||||
<ul>
|
||||
<li><a href="/three">https://npmcdn.com/three</a></li>
|
||||
<li><a href="/jquery">https://npmcdn.com/jquery</a></li>
|
||||
<li><a href="/angular-formly">https://npmcdn.com/angular-formly</a></li>
|
||||
</ul>
|
||||
|
||||
<p>Append a <code>/</code> at the end of a URL to view a listing of all the files in a package.</p>
|
||||
<ul>
|
||||
<li><a href="/lodash/">https://npmcdn.com/lodash/</a></li>
|
||||
<li><a href="/modernizr/">https://npmcdn.com/modernizr/</a></li>
|
||||
<li><a href="/react/">https://npmcdn.com/react/</a></li>
|
||||
</ul>
|
||||
|
||||
<p>You may use the special <code>/bower.zip</code> file path in packages that contain a <code>bower.json</code> file to dynamically generate a zip file that Bower can use to install the package.</p>
|
||||
<ul>
|
||||
<li><a href="/react-swap/bower.zip">https://npmcdn.com/react-swap/bower.zip</a></li>
|
||||
<li><a href="/react-collapse@1.6.3/bower.zip">https://npmcdn.com/react-collapse@1.6.3/bower.zip</a></li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Please note: <em>We do NOT recommend JavaScript libraries use Bower.</em></strong> Bower places additional burdens on JavaScript package authors for little to no gain. npmcdn is intended to make it easier to publish code, not harder, so Bower support will be removed in January 2017. Please move to npm for installing packages and stop using Bower before that time. See <a href="https://github.com/mjackson/npm-http-server#bower-support">here</a> for our rationale.</p>
|
||||
|
||||
<h3>Query Parameters</h3>
|
||||
<table cellPadding="0" cellSpacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="80px">Name</th>
|
||||
<th width="120px">Default Value</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>main</code></td>
|
||||
<td><code>main</code></td>
|
||||
<td>The name of the field in <a href="https://docs.npmjs.com/files/package.json">package.json</a> to use as the main entry point when there is no file path in the URL (e.g. <code>?main=browser</code>).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Suggested Workflow</h3>
|
||||
<p>For npm package authors, npmcdn relieves the burden of publishing your code to a CDN in addition to the npm registry. All you need to do is include your <a href="https://github.com/umdjs/umd">UMD</a> build in your npm package (not your repo, that's different!).</p>
|
||||
<p>You can do this easily using the following setup:</p>
|
||||
<ul>
|
||||
<li>Add the <code>umd</code> (or <code>dist</code>) directory to your <code>.gitignore</code> file</li>
|
||||
<li>Add the <code>umd</code> directory to your <a href="https://docs.npmjs.com/files/package.json#files">files array</a> in <code>package.json</code></li>
|
||||
<li>Use a build script to generate your UMD build in the <code>umd</code> directory when you publish</li>
|
||||
</ul>
|
||||
<p>That's it! Now when you <code>npm publish</code> you'll have a version available on npmcdn as well.</p>
|
||||
|
||||
<h3>Feedback</h3>
|
||||
<p>If you think this is useful, I'd love to hear from you. Please reach out to <a href="https://twitter.com/mjackson">@mjackson</a> with any questions/concerns.</p>
|
||||
<p>Also, please feel free to examine the source on <a href="https://github.com/mjackson/npmcdn.com">GitHub</a>.</p>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
class Home extends React.Component {
|
||||
render = () =>
|
||||
<div className="wrapper" dangerouslySetInnerHTML={{ __html: contentHTML }}/>
|
||||
}
|
||||
|
||||
export default Home
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
npmcdn is a fast, global [content-delivery network](https://en.wikipedia.org/wiki/Content_delivery_network) for stuff that is published to [npm](https://www.npmjs.com/). Use it to quickly and easily load files using a simple URL like `https://npmcdn.com/package@version/file`.
|
||||
|
||||
A few examples:
|
||||
|
||||
* [https://npmcdn.com/react@15.0.1/dist/react.min.js](/react@15.0.1/dist/react.min.js)
|
||||
* [https://npmcdn.com/react-dom@15.0.1/dist/react-dom.min.js](/react-dom@15.0.1/dist/react-dom.min.js)
|
||||
* [https://npmcdn.com/history@1.12.5/umd/History.min.js](/history@1.12.5/umd/History.min.js)
|
||||
|
||||
You may also use a [tag](https://docs.npmjs.com/cli/dist-tag) or [version range](https://docs.npmjs.com/misc/semver) instead of a fixed version number, or omit the version/tag entirely to use the `latest` tag.
|
||||
|
||||
* [https://npmcdn.com/react@^0.14/dist/react.min.js](/react@^0.14/dist/react.min.js)
|
||||
* [https://npmcdn.com/react/dist/react.min.js](/react/dist/react.min.js)
|
||||
|
||||
If you omit the file path, the [main module](https://docs.npmjs.com/files/package.json#main) will be served. This is especially useful for loading libaries that publish a UMD build as their main module.
|
||||
|
||||
* [https://npmcdn.com/jquery](/jquery)
|
||||
* [https://npmcdn.com/angular-formly](/angular-formly)
|
||||
* [https://npmcdn.com/three](/three)
|
||||
|
||||
Append a `/` at the end of a URL to view a listing of all the files in a package.
|
||||
|
||||
* [https://npmcdn.com/lodash/](/lodash/)
|
||||
* [https://npmcdn.com/modernizr/](/modernizr/)
|
||||
* [https://npmcdn.com/react/](/react/)
|
||||
|
||||
You may use the special `/bower.zip` file path in packages that contain a `bower.json` file to dynamically generate a zip file that Bower can use to install the package.
|
||||
|
||||
* [https://npmcdn.com/react-swap/bower.zip](/react-swap/bower.zip)
|
||||
* [https://npmcdn.com/react-collapse@1.6.3/bower.zip](/react-collapse@1.6.3/bower.zip)
|
||||
|
||||
**_We do NOT recommend JavaScript libraries use Bower._** Bower places additional burdens on JavaScript package authors for little to no gain. npmcdn is intended to make it easier to publish code, not harder, so Bower support will be removed in January 2017\. Please move to npm for installing packages and stop using Bower before that time. See [here](https://github.com/mjackson/npm-http-server#bower-support) for our rationale.
|
||||
|
||||
### Query Parameters
|
||||
|
||||
<table cellpadding="0" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="80px">Name</th>
|
||||
<th width="120px">Default Value</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>`main`</td>
|
||||
<td>`main`</td>
|
||||
<td>The name of the field in [package.json](https://docs.npmjs.com/files/package.json) to use as the main entry point when there is no file path in the URL (e.g. `?main=browser`).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
**All other query parameters are invalid** and will result in a `403 Forbidden` response. This helps preserve the integrity of the cache for URLs with valid query strings.
|
||||
|
||||
### Suggested Workflow
|
||||
|
||||
For npm package authors, npmcdn relieves the burden of publishing your code to a CDN in addition to the npm registry. All you need to do is include your [UMD](https://github.com/umdjs/umd) build in your npm package (not your repo, that's different!).
|
||||
|
||||
You can do this easily using the following setup:
|
||||
|
||||
* Add the `umd` (or `dist`) directory to your `.gitignore` file
|
||||
* Add the `umd` directory to your [files array](https://docs.npmjs.com/files/package.json#files) in `package.json`
|
||||
* Use a build script to generate your UMD build in the `umd` directory when you publish
|
||||
|
||||
That's it! Now when you `npm publish` you'll have a version available on npmcdn as well.
|
|
@ -0,0 +1,84 @@
|
|||
import React, { PropTypes } from 'react'
|
||||
import { Motion, spring } from 'react-motion'
|
||||
import { findDOMNode } from 'react-dom'
|
||||
import Window from './Window'
|
||||
|
||||
class Layout extends React.Component {
|
||||
state = {
|
||||
underlineLeft: 0,
|
||||
underlineWidth: 0,
|
||||
useSpring: false
|
||||
}
|
||||
|
||||
adjustUnderline = (useSpring = false) => {
|
||||
let itemIndex
|
||||
switch (this.props.location.pathname) {
|
||||
case '/about':
|
||||
itemIndex = 2
|
||||
break
|
||||
case '/stats':
|
||||
itemIndex = 1
|
||||
break
|
||||
case '/':
|
||||
default:
|
||||
itemIndex = 0
|
||||
}
|
||||
|
||||
const itemNodes = findDOMNode(this).querySelectorAll('.underlist > li')
|
||||
const currentNode = itemNodes[itemIndex]
|
||||
|
||||
this.setState({
|
||||
underlineLeft: currentNode.offsetLeft,
|
||||
underlineWidth: currentNode.offsetWidth,
|
||||
useSpring
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount = () =>
|
||||
this.adjustUnderline()
|
||||
|
||||
componentDidUpdate = (prevProps) => {
|
||||
if (prevProps.location.pathname !== this.props.location.pathname)
|
||||
this.adjustUnderline(true)
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const { underlineLeft, underlineWidth, useSpring } = this.state
|
||||
|
||||
const style = {
|
||||
left: useSpring ? spring(underlineLeft, { stiffness: 220 }) : underlineLeft,
|
||||
width: useSpring ? spring(underlineWidth) : underlineWidth
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Window onResize={this.adjustUnderline}/>
|
||||
<header>
|
||||
<h1>npmcdn</h1>
|
||||
<nav>
|
||||
<ol className="underlist">
|
||||
<li><a href="#/">Home</a></li>
|
||||
<li><a href="#/stats">Stats</a></li>
|
||||
<li><a href="#/about">About</a></li>
|
||||
</ol>
|
||||
<Motion defaultStyle={{ left: underlineLeft, width: underlineWidth }} style={style}>
|
||||
{s => (
|
||||
<div
|
||||
className="underlist-underline"
|
||||
style={{
|
||||
WebkitTransform: `translate3d(${s.left}px,0,0)`,
|
||||
transform: `translate3d(${s.left}px,0,0)`,
|
||||
width: s.width
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Motion>
|
||||
</nav>
|
||||
</header>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Layout
|
|
@ -0,0 +1,39 @@
|
|||
import React, { PropTypes } from 'react'
|
||||
import { parseNumber, formatNumber } from '../NumberUtils'
|
||||
|
||||
class NumberTextInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.number,
|
||||
parseNumber: PropTypes.func,
|
||||
formatNumber: PropTypes.func
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
value: 0,
|
||||
parseNumber,
|
||||
formatNumber
|
||||
}
|
||||
|
||||
componentWillMount = () =>
|
||||
this.setState({ value: this.props.value })
|
||||
|
||||
handleChange = (event) => {
|
||||
const value = this.props.parseNumber(event.target.value)
|
||||
|
||||
this.setState({ value }, () => {
|
||||
if (this.props.onChange)
|
||||
this.props.onChange(value)
|
||||
})
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const { value } = this.state
|
||||
const displayValue = this.props.formatNumber(value)
|
||||
|
||||
return (
|
||||
<input {...this.props} type="text" value={displayValue} onChange={this.handleChange}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default NumberTextInput
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react'
|
||||
import history from '../history'
|
||||
import Layout from './Layout'
|
||||
import About from './About'
|
||||
import Stats from './Stats'
|
||||
import Home from './Home'
|
||||
|
||||
const findMatchingComponents = (location) => {
|
||||
let components
|
||||
switch (location.pathname) {
|
||||
case '/about':
|
||||
components = [ Layout, About ]
|
||||
break
|
||||
case '/stats':
|
||||
components = [ Layout, Stats ]
|
||||
break
|
||||
case '/':
|
||||
default:
|
||||
components = [ Layout, Home ]
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
const renderNestedComponents = (components, props) =>
|
||||
components.reduceRight(
|
||||
(children, component) => React.createElement(component, { ...props, children }),
|
||||
undefined
|
||||
)
|
||||
|
||||
class Router extends React.Component {
|
||||
state = {
|
||||
location: history.getCurrentLocation()
|
||||
}
|
||||
|
||||
componentDidMount = () =>
|
||||
this.unlisten = history.listen(location => {
|
||||
this.setState({ location })
|
||||
})
|
||||
|
||||
componentWillUnmount = () =>
|
||||
this.unlisten()
|
||||
|
||||
render = () => {
|
||||
const { location } = this.state
|
||||
const components = findMatchingComponents(location)
|
||||
return renderNestedComponents(components, { location })
|
||||
}
|
||||
}
|
||||
|
||||
export default Router
|
|
@ -0,0 +1,142 @@
|
|||
import React, { PropTypes } from 'react'
|
||||
import { findDOMNode } from 'react-dom'
|
||||
import formatBytes from 'byte-size'
|
||||
import formatDate from 'date-fns/format'
|
||||
import parseDate from 'date-fns/parse'
|
||||
import { formatNumber, formatPercent } from '../NumberUtils'
|
||||
import { ContinentsIndex, CountriesIndex, getCountriesByContinent } from '../CountryUtils'
|
||||
import NumberTextInput from './NumberTextInput'
|
||||
|
||||
const getSum = (data, countries) =>
|
||||
countries.reduce((n, country) => n + (data[country] || 0), 0)
|
||||
|
||||
const addValues = (a, b) => {
|
||||
for (const p in b) {
|
||||
if (p in a) {
|
||||
a[p] += b[p]
|
||||
} else {
|
||||
a[p] = b[p]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Stats extends React.Component {
|
||||
static defaultProps = {
|
||||
stats: window.NPMCDN_STATS
|
||||
}
|
||||
|
||||
state = {
|
||||
minRequests: 1000000
|
||||
}
|
||||
|
||||
updateMinRequests = (value) =>
|
||||
this.setState({ minRequests: value })
|
||||
|
||||
render = () => {
|
||||
const { minRequests } = this.state
|
||||
const { stats } = this.props
|
||||
const { timeseries, totals } = stats
|
||||
|
||||
// Summary data
|
||||
const sinceDate = parseDate(totals.since)
|
||||
const untilDate = parseDate(totals.until)
|
||||
const uniqueVisitors = totals.uniques.all
|
||||
|
||||
const totalRequests = totals.requests.all
|
||||
const cachedRequests = totals.requests.cached
|
||||
const totalBandwidth = totals.bandwidth.all
|
||||
const httpStatus = totals.requests.http_status
|
||||
|
||||
let errorRequests = 0
|
||||
for (const status in httpStatus) {
|
||||
if (httpStatus.hasOwnProperty(status) && status >= 500)
|
||||
errorRequests += httpStatus[status]
|
||||
}
|
||||
|
||||
// By Region
|
||||
const regionRows = []
|
||||
const requestsByCountry = {}
|
||||
const bandwidthByCountry = {}
|
||||
|
||||
timeseries.forEach(ts => {
|
||||
addValues(requestsByCountry, ts.requests.country)
|
||||
addValues(bandwidthByCountry, ts.bandwidth.country)
|
||||
})
|
||||
|
||||
const byRequestsDescending = (a, b) =>
|
||||
requestsByCountry[b] - requestsByCountry[a]
|
||||
|
||||
const continentData = Object.keys(ContinentsIndex).reduce((memo, continent) => {
|
||||
const countries = getCountriesByContinent(continent)
|
||||
|
||||
memo[continent] = {
|
||||
countries,
|
||||
requests: getSum(requestsByCountry, countries),
|
||||
bandwidth: getSum(bandwidthByCountry, countries)
|
||||
}
|
||||
|
||||
return memo
|
||||
}, {})
|
||||
|
||||
const topContinents = Object.keys(continentData).sort((a, b) => {
|
||||
return continentData[b].requests - continentData[a].requests
|
||||
})
|
||||
|
||||
topContinents.forEach(continent => {
|
||||
const name = ContinentsIndex[continent]
|
||||
const { countries, requests, bandwidth } = continentData[continent]
|
||||
|
||||
if (bandwidth !== 0) {
|
||||
regionRows.push(
|
||||
<tr key={continent} className="continent-row">
|
||||
<td>{ContinentsIndex[continent]}</td>
|
||||
<td>{formatNumber(requests)} ({formatPercent(requests / totalRequests)}%)</td>
|
||||
<td>{formatBytes(bandwidth)} ({formatPercent(bandwidth / totalBandwidth)}%)</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
const topCountries = countries.sort(byRequestsDescending)
|
||||
|
||||
topCountries.forEach(country => {
|
||||
const countryRequests = requestsByCountry[country]
|
||||
const countryBandwidth = bandwidthByCountry[country]
|
||||
|
||||
if (countryRequests > minRequests) {
|
||||
regionRows.push(
|
||||
<tr key={continent + country} className="country-row">
|
||||
<td className="country-name">{CountriesIndex[country].name}</td>
|
||||
<td>{formatNumber(countryRequests)} ({formatPercent(countryRequests / totalRequests)}%)</td>
|
||||
<td>{formatBytes(countryBandwidth)} ({formatPercent(countryBandwidth / totalBandwidth)}%)</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="wrapper">
|
||||
<p>From <strong>{formatDate(sinceDate, 'MMM D')}</strong> to <strong>{formatDate(untilDate, 'MMM D')}</strong>, npmcdn served <strong>{formatNumber(totalRequests)}</strong> requests to <strong>{formatNumber(uniqueVisitors)}</strong> unique visitors, <strong>{formatPercent(cachedRequests / totalRequests, 0)}%</strong> of which came from the cache (CDN). Over the same period, <strong>{formatPercent(errorRequests / totalRequests, 4)}%</strong> of requests resulted in server error (returned an HTTP status ≥ 500).</p>
|
||||
|
||||
<h3>By Region</h3>
|
||||
|
||||
<label className="table-filter">Include countries that made at least <NumberTextInput value={minRequests} onChange={this.updateMinRequests}/> requests.</label>
|
||||
|
||||
<table cellSpacing="0" cellPadding="0" style={{ width: '100%' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Requests (% of total)</th>
|
||||
<th>Bandwidth (% of total)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{regionRows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Stats
|
|
@ -0,0 +1,26 @@
|
|||
import React, { PropTypes } from 'react'
|
||||
import { addEvent, removeEvent } from '../DOMUtils'
|
||||
|
||||
const ResizeEvent = 'resize'
|
||||
|
||||
class Window extends React.Component {
|
||||
static propTypes = {
|
||||
onResize: PropTypes.func
|
||||
}
|
||||
|
||||
handleWindowResize = () => {
|
||||
if (this.props.onResize)
|
||||
this.props.onResize()
|
||||
}
|
||||
|
||||
componentDidMount = () =>
|
||||
addEvent(window, ResizeEvent, this.handleWindowResize)
|
||||
|
||||
componentWillUnmount = () =>
|
||||
removeEvent(window, ResizeEvent, this.handleWindowResize)
|
||||
|
||||
render = () =>
|
||||
null
|
||||
}
|
||||
|
||||
export default Window
|
|
@ -0,0 +1,5 @@
|
|||
import createHistory from 'history/lib/createHashHistory'
|
||||
|
||||
const history = createHistory()
|
||||
|
||||
export default history
|
|
@ -1,8 +1,10 @@
|
|||
import React from 'react'
|
||||
import { render } from 'react-dom'
|
||||
import Home from './components/Home'
|
||||
import Router from './components/Router'
|
||||
|
||||
import './styles.css'
|
||||
|
||||
render(
|
||||
<Home/>,
|
||||
<Router/>,
|
||||
document.getElementById('app')
|
||||
)
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
body {
|
||||
font: 16px Helvetica, sans-serif;
|
||||
line-height: 1.5;
|
||||
padding: 5px 20px;
|
||||
}
|
||||
|
||||
@media all and (min-width: 660px) {
|
||||
body {
|
||||
padding: 50px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: blue;
|
||||
}
|
||||
a:visited {
|
||||
color: rebeccapurple;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
table {
|
||||
border-color: black;
|
||||
border-style: solid;
|
||||
border-width: 0 0 1px 1px;
|
||||
}
|
||||
table th, table td {
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
padding: 5px 7px;
|
||||
border-color: black;
|
||||
border-style: solid;
|
||||
border-width: 1px 1px 0 0;
|
||||
}
|
||||
|
||||
.continent-row {
|
||||
font-weight: bold;
|
||||
}
|
||||
.country-name {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.table-filter {
|
||||
display: block;
|
||||
text-align: right;
|
||||
color: #666;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.table-filter input {
|
||||
font-size: 1em;
|
||||
text-align: right;
|
||||
max-width: 100px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
header, .wrapper {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1, h2, h3, header nav {
|
||||
font-family: Futura, Helvetica, sans-serif;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 4em;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
header nav {
|
||||
margin-bottom: 4em;
|
||||
}
|
||||
header nav a:link,
|
||||
header nav a:visited {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.underlist {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.underlist li {
|
||||
margin: 0 5px;
|
||||
padding: 0 5px;
|
||||
|
||||
flex-basis: auto;
|
||||
}
|
||||
.underlist a:link {
|
||||
text-decoration: none;
|
||||
}
|
||||
.underlist-underline {
|
||||
height: 4px;
|
||||
background-color: black;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.sponsor-logos {
|
||||
margin: 2em 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.sponsor-logo {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
max-width: 80%;
|
||||
}
|
||||
.sponsor-logo img {
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.bar-graph {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.bar-graph svg {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.y-axis line,
|
||||
.x-axis line {
|
||||
stroke: black;
|
||||
stroke-width: 1px;
|
||||
}
|
|
@ -2,30 +2,32 @@ import fs from 'fs'
|
|||
import invariant from 'invariant'
|
||||
import webpack from 'webpack'
|
||||
|
||||
const createAssets = (webpackStats) => {
|
||||
const createBundle = (webpackStats) => {
|
||||
const { publicPath, assetsByChunkName } = webpackStats
|
||||
|
||||
const createURL = (asset) =>
|
||||
webpackStats.publicPath + asset
|
||||
publicPath + asset
|
||||
|
||||
const getAssets = (chunkName = 'main') => {
|
||||
const assets = webpackStats.assetsByChunkName[chunkName] || []
|
||||
return Array.isArray(assets) ? assets : [ assets ]
|
||||
}
|
||||
const getAssets = (chunks = [ 'main' ]) =>
|
||||
(Array.isArray(chunks) ? chunks : [ chunks ]).reduce((memo, chunk) => (
|
||||
memo.concat(assetsByChunkName[chunk] || [])
|
||||
), [])
|
||||
|
||||
const getScriptURLs = (chunkName = 'main') =>
|
||||
getAssets(chunkName)
|
||||
const getScriptAssets = (...args) =>
|
||||
getAssets(...args)
|
||||
.filter(asset => (/\.js$/).test(asset))
|
||||
.map(createURL)
|
||||
|
||||
const getStyleURLs = (chunkName = 'main') =>
|
||||
getAssets(chunkName)
|
||||
const getStyleAssets = (...args) =>
|
||||
getAssets(...args)
|
||||
.filter(asset => (/\.css$/).test(asset))
|
||||
.map(createURL)
|
||||
|
||||
return {
|
||||
createURL,
|
||||
getAssets,
|
||||
getScriptURLs,
|
||||
getStyleURLs
|
||||
getScriptAssets,
|
||||
getStyleAssets
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,7 +48,7 @@ export const staticAssets = (webpackStatsFile) => {
|
|||
)
|
||||
}
|
||||
|
||||
const assets = createAssets(stats)
|
||||
const assets = createBundle(stats)
|
||||
|
||||
return (req, res, next) => {
|
||||
req.assets = assets
|
||||
|
@ -62,7 +64,7 @@ export const staticAssets = (webpackStatsFile) => {
|
|||
export const assetsCompiler = (webpackCompiler) => {
|
||||
let assets
|
||||
webpackCompiler.plugin('done', (stats) => {
|
||||
assets = createAssets(stats.toJson())
|
||||
assets = createBundle(stats.toJson())
|
||||
})
|
||||
|
||||
return (req, res, next) => {
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import 'isomorphic-fetch'
|
||||
import { createStack, createFetch, header, base, query, parseJSON, onResponse } from 'http-client'
|
||||
import invariant from 'invariant'
|
||||
|
||||
const CloudFlareKey = process.env.CLOUDFLARE_KEY
|
||||
const CloudFlareEmail = process.env.CLOUDFLARE_EMAIL
|
||||
|
||||
invariant(
|
||||
CloudFlareKey,
|
||||
'Missing $CLOUDFLARE_KEY environment variable'
|
||||
)
|
||||
|
||||
invariant(
|
||||
CloudFlareEmail,
|
||||
'Missing $CLOUDFLARE_EMAIL environment variable'
|
||||
)
|
||||
|
||||
const createRangeQuery = (since, until) =>
|
||||
query({
|
||||
since: since.toISOString(),
|
||||
until: until.toISOString()
|
||||
})
|
||||
|
||||
const createNameQuery = (name) =>
|
||||
query({ name })
|
||||
|
||||
const getResult = () =>
|
||||
createStack(
|
||||
parseJSON(),
|
||||
onResponse(response => response.jsonData.result)
|
||||
)
|
||||
|
||||
const commonStack = createStack(
|
||||
header('X-Auth-Key', CloudFlareKey),
|
||||
header('X-Auth-Email', CloudFlareEmail),
|
||||
base('https://api.cloudflare.com/client/v4'),
|
||||
getResult()
|
||||
)
|
||||
|
||||
export const getZones = (domainName) =>
|
||||
createFetch(
|
||||
commonStack,
|
||||
createNameQuery(domainName)
|
||||
)('/zones')
|
||||
|
||||
export const getZoneAnalyticsDashboard = (zone, since, until) =>
|
||||
createFetch(
|
||||
commonStack,
|
||||
createRangeQuery(since, until)
|
||||
)(`/zones/${zone.id}/analytics/dashboard`)
|
|
@ -1,16 +1,43 @@
|
|||
import React from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { getZones, getZoneAnalyticsDashboard } from './CloudFlare'
|
||||
import HomePage from './components/HomePage'
|
||||
|
||||
const OneMinute = 1000 * 60
|
||||
const ThirtyDays = OneMinute * 60 * 24 * 30
|
||||
const DOCTYPE = '<!DOCTYPE html>'
|
||||
|
||||
export const sendHomePage = (req, res) => {
|
||||
const fetchStats = (callback) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
callback(null, require('./stats.json'))
|
||||
} else {
|
||||
getZones('npmcdn.com')
|
||||
.then(zones => {
|
||||
const zone = zones[0]
|
||||
const since = new Date(Date.now() - ThirtyDays)
|
||||
const until = new Date(Date.now() - OneMinute)
|
||||
|
||||
return getZoneAnalyticsDashboard(zone, since, until).then(result => {
|
||||
callback(null, result)
|
||||
})
|
||||
})
|
||||
.catch(callback)
|
||||
}
|
||||
}
|
||||
|
||||
export const sendHomePage = (req, res, next) => {
|
||||
const props = {
|
||||
styles: req.assets.getStyleURLs('home'),
|
||||
scripts: req.assets.getScriptURLs('home')
|
||||
styles: req.assets.getStyleAssets([ 'vendor', 'home' ]),
|
||||
scripts: req.assets.getScriptAssets([ 'vendor', 'home' ])
|
||||
}
|
||||
|
||||
res.send(
|
||||
DOCTYPE + renderToStaticMarkup(<HomePage {...props}/>)
|
||||
)
|
||||
fetchStats((error, stats) => {
|
||||
if (error) {
|
||||
next(error)
|
||||
} else {
|
||||
res.send(
|
||||
DOCTYPE + renderToStaticMarkup(<HomePage {...props} stats={stats}/>)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
import React, { PropTypes } from 'react'
|
||||
|
||||
const assetType = PropTypes.string
|
||||
class HomePage extends React.Component {
|
||||
static propTypes = {
|
||||
styles: PropTypes.arrayOf(PropTypes.string),
|
||||
scripts: PropTypes.arrayOf(PropTypes.string),
|
||||
stats: PropTypes.object
|
||||
}
|
||||
|
||||
const HomePage = React.createClass({
|
||||
propTypes: {
|
||||
styles: PropTypes.arrayOf(assetType),
|
||||
scripts: PropTypes.arrayOf(assetType)
|
||||
},
|
||||
static defaultProps = {
|
||||
styles: [],
|
||||
scripts: [],
|
||||
stats: {}
|
||||
}
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
styles: [],
|
||||
scripts: []
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const { styles, scripts } = this.props
|
||||
render = () => {
|
||||
const { styles, scripts, stats } = this.props
|
||||
|
||||
return (
|
||||
<html>
|
||||
|
@ -26,6 +24,7 @@ const HomePage = React.createClass({
|
|||
<meta name="timestamp" content={(new Date).toISOString()}/>
|
||||
<title>npmcdn</title>
|
||||
{styles.map(style => <link key={style} rel="stylesheet" href={style}/>)}
|
||||
<script dangerouslySetInnerHTML={{ __html: `window.NPMCDN_STATS=${JSON.stringify(stats)}` }}/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"/>
|
||||
|
@ -34,6 +33,6 @@ const HomePage = React.createClass({
|
|||
</html>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default HomePage
|
||||
|
|
14
package.json
14
package.json
|
@ -5,9 +5,8 @@
|
|||
"start": "heroku local -f Procfile.local",
|
||||
"build": "npm run build-assets && npm run build-lib",
|
||||
"build-assets": "NODE_ENV=production webpack -p --json > stats.json",
|
||||
"build-lib": "rimraf lib && babel ./modules -d lib",
|
||||
"build-lib": "rimraf lib && babel ./modules -d lib --copy-files",
|
||||
"heroku-postbuild": "npm run build"
|
||||
|
||||
},
|
||||
"dependencies": {
|
||||
"autoprefixer": "^6.3.6",
|
||||
|
@ -16,18 +15,29 @@
|
|||
"babel-preset-es2015": "^6.6.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"babel-preset-stage-1": "^6.5.0",
|
||||
"byte-size": "^2.0.0",
|
||||
"cors": "^2.7.1",
|
||||
"countries-list": "^1.1.0",
|
||||
"css-loader": "^0.23.1",
|
||||
"date-fns": "^1.0.0",
|
||||
"errorhandler": "^1.4.3",
|
||||
"express": "^4.13.4",
|
||||
"extract-text-webpack-plugin": "^1.0.1",
|
||||
"file-loader": "^0.8.5",
|
||||
"history": "^3.0.0-2",
|
||||
"html-loader": "^0.4.3",
|
||||
"http-client": "^4.0.1",
|
||||
"invariant": "^2.2.1",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"json-loader": "^0.5.4",
|
||||
"markdown-loader": "^0.1.7",
|
||||
"morgan": "^1.7.0",
|
||||
"npm-http-server": "^2.11.0",
|
||||
"on-finished": "^2.3.0",
|
||||
"postcss-loader": "^0.9.1",
|
||||
"react": "^15.0.2",
|
||||
"react-dom": "^15.0.2",
|
||||
"react-motion": "^0.4.3",
|
||||
"redis": "^2.6.0-1",
|
||||
"rimraf": "^2.5.2",
|
||||
"style-loader": "^0.13.1",
|
||||
|
|
|
@ -3,15 +3,16 @@ const webpack = require('webpack')
|
|||
const autoprefixer = require('autoprefixer')
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
|
||||
const AssetURLPrefix = '[hash:8]/'
|
||||
const outputPrefix = '[chunkhash:8]-'
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
vendor: [ 'react' ],
|
||||
home: path.resolve(__dirname, 'modules/client/home.js')
|
||||
},
|
||||
|
||||
output: {
|
||||
filename: `${AssetURLPrefix}[name].js`,
|
||||
filename: `${outputPrefix}[name].js`,
|
||||
path: path.resolve(__dirname, 'public/__assets__'),
|
||||
publicPath: '/__assets__/'
|
||||
},
|
||||
|
@ -19,13 +20,17 @@ module.exports = {
|
|||
module: {
|
||||
loaders: [
|
||||
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel' },
|
||||
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style', 'css!postcss') }
|
||||
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style', 'css!postcss') },
|
||||
{ test: /\.json$/, loader: 'json' },
|
||||
{ test: /\.png$/, loader: 'file' },
|
||||
{ test: /\.md$/, loader: 'html!markdown' }
|
||||
]
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.optimize.OccurrenceOrderPlugin(),
|
||||
new ExtractTextPlugin(`${AssetURLPrefix}styles.css`),
|
||||
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor' }),
|
||||
new ExtractTextPlugin(`${outputPrefix}styles.css`),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue