Rewrite frontend using create-react-app
This commit is contained in:
9
client/About.js
Normal file
9
client/About.js
Normal file
@ -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
|
||||
50
client/About.md
Normal file
50
client/About.md
Normal file
@ -0,0 +1,50 @@
|
||||
unpkg is an [open source](https://github.com/mjackson/unpkg) 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://reacttraining.com).
|
||||
|
||||
<div class="about-logos">
|
||||
<div class="about-logo">
|
||||
<a href="https://reacttraining.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@reacttraining.com) if interested.
|
||||
|
||||
### Sponsors
|
||||
|
||||
The fast, global infrastructure that powers unpkg is generously donated by [Cloudflare](https://www.cloudflare.com) and [Heroku](https://www.heroku.com).
|
||||
|
||||
<div class="about-logos">
|
||||
<div class="about-logo">
|
||||
<a href="https://www.cloudflare.com"><img src="./CloudflareLogo.png"></a>
|
||||
</div>
|
||||
<div class="about-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 unpkg.
|
||||
|
||||
### 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.
|
||||
|
||||
### Support
|
||||
|
||||
unpkg 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 unpkg 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.
|
||||
|
||||
unpkg is not affiliated with or supported by npm, Inc. in any way. Please do not contact npm for help with unpkg.
|
||||
|
||||
### Abuse
|
||||
|
||||
unpkg blacklists some packages to prevent abuse. If you find a malicious package on npm, please take a moment to add it to [our blacklist](https://github.com/mjackson/unpkg/blob/master/modules/PackageBlacklist.js)!
|
||||
|
||||
### 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.
|
||||
20
client/App.js
Normal file
20
client/App.js
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import { HashRouter as Router, Switch, Route } from 'react-router-dom'
|
||||
import Layout from './Layout'
|
||||
import About from './About'
|
||||
import Stats from './Stats'
|
||||
import Home from './Home'
|
||||
|
||||
const App = () => (
|
||||
<Router>
|
||||
<Layout>
|
||||
<Switch>
|
||||
<Route path="/stats" component={Stats}/>
|
||||
<Route path="/about" component={About}/>
|
||||
<Route path="/" component={Home}/>
|
||||
</Switch>
|
||||
</Layout>
|
||||
</Router>
|
||||
)
|
||||
|
||||
export default App
|
||||
8
client/App.test.js
Normal file
8
client/App.test.js
Normal file
@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const div = document.createElement('div');
|
||||
ReactDOM.render(<App />, div);
|
||||
});
|
||||
BIN
client/CloudflareLogo.png
Normal file
BIN
client/CloudflareLogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
13
client/CountryUtils.js
Normal file
13
client/CountryUtils.js
Normal file
@ -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
|
||||
}
|
||||
15
client/DOMUtils.js
Normal file
15
client/DOMUtils.js
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
BIN
client/HerokuLogo.png
Normal file
BIN
client/HerokuLogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
12
client/Home.js
Normal file
12
client/Home.js
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
import contentHTML from './Home.md'
|
||||
|
||||
class Home extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="wrapper" dangerouslySetInnerHTML={{ __html: contentHTML }}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Home
|
||||
62
client/Home.md
Normal file
62
client/Home.md
Normal file
@ -0,0 +1,62 @@
|
||||
unpkg 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:
|
||||
|
||||
<div style="text-align:center">`https://unpkg.com/package@version/file`</div>
|
||||
|
||||
A few examples:
|
||||
|
||||
* [https://unpkg.com/react@15.3.1/dist/react.min.js](/react@15.3.1/dist/react.min.js)
|
||||
* [https://unpkg.com/react-dom@15.3.1/dist/react-dom.min.js](/react-dom@15.3.1/dist/react-dom.min.js)
|
||||
* [https://unpkg.com/history@4.2.0/umd/history.min.js](/history@4.2.0/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://unpkg.com/react@^0.14/dist/react.min.js](/react@^0.14/dist/react.min.js)
|
||||
* [https://unpkg.com/react/dist/react.min.js](/react/dist/react.min.js)
|
||||
|
||||
If you omit the file path, unpkg will try to serve [the `browser` bundle](https://github.com/defunctzombie/package-browser-field-spec) if present, the [`main` module](https://docs.npmjs.com/files/package.json#main) otherwise.
|
||||
|
||||
* [https://unpkg.com/jquery](/jquery)
|
||||
* [https://unpkg.com/angular-formly](/angular-formly)
|
||||
* [https://unpkg.com/three](/three)
|
||||
|
||||
Append a `/` at the end of a URL to view a listing of all the files in a package.
|
||||
|
||||
* [https://unpkg.com/lodash/](/lodash/)
|
||||
* [https://unpkg.com/modernizr/](/modernizr/)
|
||||
* [https://unpkg.com/react/](/react/)
|
||||
|
||||
### 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>`unpkg`, `browser`, `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.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`json`</td>
|
||||
<td>`undefined`</td>
|
||||
<td>Return a recursive list of metadata about all the files in a directory as JSON (e.g. `/any/path/?json`). Note: this only works for directories.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Suggested Workflow
|
||||
|
||||
For npm package authors, unpkg 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 unpkg as well.
|
||||
91
client/Layout.js
Normal file
91
client/Layout.js
Normal file
@ -0,0 +1,91 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import { findDOMNode } from 'react-dom'
|
||||
import { Motion, spring } from 'react-motion'
|
||||
import { withRouter, Link } from 'react-router-dom'
|
||||
import WindowSize from './WindowSize'
|
||||
|
||||
class Layout extends React.Component {
|
||||
static propTypes = {
|
||||
location: PropTypes.object,
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
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>
|
||||
<WindowSize onChange={this.adjustUnderline}/>
|
||||
<header>
|
||||
<h1>unpkg</h1>
|
||||
<nav>
|
||||
<ol className="underlist">
|
||||
<li><Link to="/">Home</Link></li>
|
||||
<li><Link to="/stats">Stats</Link></li>
|
||||
<li><Link to="/about">About</Link></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 withRouter(Layout)
|
||||
41
client/NumberTextInput.js
Normal file
41
client/NumberTextInput.js
Normal file
@ -0,0 +1,41 @@
|
||||
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,
|
||||
onChange: 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 { parseNumber, formatNumber, ...props } = this.props // eslint-disable-line no-unused-vars
|
||||
const displayValue = formatNumber(value)
|
||||
|
||||
return (
|
||||
<input {...props} type="text" value={displayValue} onChange={this.handleChange}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default NumberTextInput
|
||||
15
client/NumberUtils.js
Normal file
15
client/NumberUtils.js
Normal file
@ -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) || 0
|
||||
|
||||
export const formatPercent = (n, fixed = 1) =>
|
||||
String((n.toPrecision(2) * 100).toFixed(fixed))
|
||||
BIN
client/ReactTrainingLogo.png
Normal file
BIN
client/ReactTrainingLogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
146
client/Stats.js
Normal file
146
client/Stats.js
Normal file
@ -0,0 +1,146 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import formatBytes from 'pretty-bytes'
|
||||
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 propTypes = {
|
||||
serverData: PropTypes.object
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
serverData: window.serverData
|
||||
}
|
||||
|
||||
state = {
|
||||
minRequests: 5000000
|
||||
}
|
||||
|
||||
updateMinRequests = (value) => {
|
||||
this.setState({ minRequests: value })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { minRequests } = this.state
|
||||
const stats = this.props.serverData.cloudflareStats
|
||||
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 continentName = ContinentsIndex[continent]
|
||||
const { countries, requests, bandwidth } = continentData[continent]
|
||||
|
||||
if (bandwidth !== 0) {
|
||||
regionRows.push(
|
||||
<tr key={continent} className="continent-row">
|
||||
<td>{continentName}</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>, unpkg 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
|
||||
32
client/WindowSize.js
Normal file
32
client/WindowSize.js
Normal file
@ -0,0 +1,32 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import { addEvent, removeEvent } from './DOMUtils'
|
||||
|
||||
const ResizeEvent = 'resize'
|
||||
|
||||
class WindowSize extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func
|
||||
}
|
||||
|
||||
handleWindowResize = () => {
|
||||
if (this.props.onChange)
|
||||
this.props.onChange({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
addEvent(window, ResizeEvent, this.handleWindowResize)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
removeEvent(window, ResizeEvent, this.handleWindowResize)
|
||||
}
|
||||
|
||||
render() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default WindowSize
|
||||
127
client/index.css
Normal file
127
client/index.css
Normal file
@ -0,0 +1,127 @@
|
||||
body {
|
||||
font-size: 16px;
|
||||
font-family: -apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
Helvetica,
|
||||
Arial,
|
||||
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;
|
||||
}
|
||||
h3 {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.about-logos {
|
||||
margin: 2em 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.about-logo {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
max-width: 80%;
|
||||
}
|
||||
.about-logo img {
|
||||
max-width: 60%;
|
||||
}
|
||||
9
client/index.js
Normal file
9
client/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.render(
|
||||
<App />,
|
||||
document.getElementById('root')
|
||||
);
|
||||
Reference in New Issue
Block a user