Initial commit.

pull/1/head
Icedream 2017-08-19 03:25:18 +02:00
commit 9e324a1f94
Signed by: icedream
GPG Key ID: 1573F6D8EFE4D0CF
25 changed files with 12249 additions and 0 deletions

11
.dockerignore Normal file
View File

@ -0,0 +1,11 @@
###
.git*
Dockerfile
.dockerignore
docker-compose.yml
*.md

19
.eslintrc.yml Normal file
View File

@ -0,0 +1,19 @@
extends: airbnb
parser: babel-eslint
plugins:
- babel
- json
env:
browser: true
node: true
es6: true
rules:
no-underscore-dangle: 'off'
no-plusplus:
- error
- allowForLoopAfterthoughts: true
react/no-array-index-key: 0

28
.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# Deployed apps should consider commenting this line out:
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
node_modules
# Webpack output
/dist

16
.travis.yml Normal file
View File

@ -0,0 +1,16 @@
sudo: false
language: node_js
node_js:
- 6
before_install:
- npm install codecov.io coveralls
after_success:
- cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js
- cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
branches:
only:
- master

4
Dockerfile.dist Normal file
View File

@ -0,0 +1,4 @@
FROM icedream/caddy
WORKDIR /data
COPY ./dist/ /data

29
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,29 @@
node("docker && linux && amd64") {
checkout scm
docker.image("node:8.2.1").inside {
// Install dependencies
sh "npm install"
// Build website with npm
sh "npm run build"
archive "dist/**"
}
// Build docker image to be deployed
def image = docker.build("docker.dreamnetwork.oss:5000/icedream/carl-kittelberger-website:${env.BRANCH_NAME ?: "latest"}", "-f Dockerfile.dist .")
// @NOTE - https://issues.jenkins-ci.org/browse/JENKINS-42152?focusedCommentId=307976&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-307976
image.push()
if (env.BRANCH_NAME) {
switch(env.BRANCH_NAME) {
case "master":
image.push("latest")
break
default:
image.push()
break
}
}
image.push("${sh(script: "git describe --tags --always", returnStdout: true).trim()}")
}

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# VIzon countdown website
Source code for vizon-countdown.icedream.pw.

37
nwb.config.js Normal file
View File

@ -0,0 +1,37 @@
module.exports = {
type: 'react-app',
babel: {
loose: false,
presets: [
'babel-preset-env',
],
plugins: [
'babel-plugin-transform-class-properties',
],
},
webpack: {
autoprefixer: '> 1%, last 2 versions, Firefox ESR, ios >= 8',
extra: {
resolve: {
extensions: [
'.jsx',
],
},
},
publicPath: '',
rules: {
babel: {
test: /\.jsx?/,
},
'sass-css': {
modules: true,
localIdentName: '[name]__[local]__[hash:base64:5]',
},
},
},
};

11609
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

54
package.json Normal file
View File

@ -0,0 +1,54 @@
{
"name": "vizon-countdown-website",
"version": "0.1.0",
"description": "Website for a countdown to the next draw on VIzon",
"private": true,
"main": "src/index.jsx",
"scripts": {
"build": "nwb build-react-app ./src",
"clean": "nwb clean-app",
"start": "nwb serve-react-app ./src",
"test": "nwb test-react",
"test:coverage": "nwb test-react --coverage",
"test:watch": "nwb test-react --server",
"lint": "eslint ."
},
"repository": {
"type": "git",
"url": "ssh://git@git.icedream.tech:2222/icedream/vizon-countdown-website.git"
},
"keywords": [
"website",
"vizon",
"rizon",
"draw",
"countdown"
],
"author": "Carl Kittelberger <icedream@icedream.pw>",
"license": "UNLICENSED",
"devDependencies": {
"babel-eslint": "^7.2.3",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-preset-env": "^1.6.0",
"eslint": "^4.4.1",
"eslint-config-airbnb": "^15.1.0",
"eslint-plugin-babel": "^4.1.2",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-json": "^1.2.0",
"eslint-plugin-jsx-a11y": "^6.0.2",
"eslint-plugin-react": "^7.2.1",
"file-loader": "^0.11.2",
"nwb": "^0.18.10",
"nwb-sass": "^0.8.1",
"svg-inline-loader": "^0.8.0"
},
"dependencies": {
"moment": "^2.18.1",
"normalize-scss": "^7.0.0",
"prop-types": "^15.5.10",
"react": "^15.6.1",
"react-dom": "^15.6.1",
"react-helmet": "^5.1.3",
"react-router": "^4.1.2"
}
}

73
src/App.jsx Normal file
View File

@ -0,0 +1,73 @@
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import Countdown from './Countdown';
import Header from './Header';
import ProgressCircle from './ProgressCircle';
import getUpcomingDate from './getUpcomingDate';
import style from './App.sass';
class App extends React.Component {
static propTypes = {
getNow: PropTypes.func,
weekdays: PropTypes.arrayOf(PropTypes.string),
}
static defaultProps = {
getNow: () => moment(),
weekdays: [
'tuesday',
'thursday',
'saturday',
],
}
componentDidMount() {
this.interval = setInterval(this.updateUpcomingDate.bind(this), 1000);
this.updateUpcomingDate();
}
componentWillUnmount() {
clearInterval(this.interval);
}
calculateUpcomingDate() {
const { getNow, weekdays } = this.props;
const now = getNow();
return moment.min(
...weekdays.map(weekday => getUpcomingDate(now, weekday)),
);
}
updateUpcomingDate() {
this.setState({ nextUpcomingDate: this.calculateUpcomingDate() });
}
render() {
if (!this.state) {
return null;
}
const { nextUpcomingDate } = this.state;
return (
<div className={style.app}>
<Header>
VIzon countdown
</Header>
<div className={style.content}>
<div className={style.contentContainer}>
<p>
The next VIzon draw is on {nextUpcomingDate.format('dddd')}, {nextUpcomingDate.format('L LT')}.
</p>
<Countdown date={nextUpcomingDate} />
</div>
</div>
</div>
);
}
}
export default App;

19
src/App.sass Normal file
View File

@ -0,0 +1,19 @@
.app
background-color: #f8f8f8
min-height: 100vh
font-size: 24px
display: flex
flex-direction: column
justify-content: stretch
align-items: center
.content
color: #222
line-height: 1.5
width: 100%
max-width: 1024px
padding: 1em 0
box-sizing: content-box
flex-grow: 1
text-align: center

97
src/Countdown.jsx Normal file
View File

@ -0,0 +1,97 @@
import React from 'react';
import moment from 'moment';
import PropTypes from 'prop-types';
import DigitBlock from './DigitBlock';
import ProgressCircle from './ProgressCircle';
import style from './Countdown.sass';
export default class Countdown extends React.Component {
static propTypes = {
date: PropTypes.object.isRequired,
locale: PropTypes.string,
minimumIntegerDigits: PropTypes.number,
useGrouping: PropTypes.bool,
getNow: PropTypes.func,
};
static defaultProps = {
locale: undefined,
minimumIntegerDigits: 2,
useGrouping: false,
getNow: () => moment(),
}
componentDidMount() {
this.tick();
this.interval = setInterval(this.tick.bind(this), 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
calculateDiff() {
const { getNow, date } = this.props;
return date.diff(getNow());
}
tick() {
this.setState({ duration: this.calculateDiff() });
}
render() {
if (!this.state) {
return null;
}
const { duration } = this.state;
const durationMoment = moment.duration(duration);
const days = durationMoment.days();
const hours = durationMoment.hours();
const minutes = durationMoment.minutes();
const seconds = durationMoment.seconds();
const size = 160; // @HACK
const { locale, minimumIntegerDigits, useGrouping } = this.props;
return (
<div className={style.countdown}>
<ProgressCircle size={size} max={3} progress={days} stroke="rgb(60, 90, 128)">
<DigitBlock suffix="d">
{days.toLocaleString(locale, {
minimumIntegerDigits,
useGrouping,
})}
</DigitBlock>
</ProgressCircle>
<ProgressCircle size={size} max={24} progress={hours} stroke="rgb(60, 144, 60)">
<DigitBlock suffix="h">
{hours.toLocaleString(locale, {
minimumIntegerDigits,
useGrouping,
})}
</DigitBlock>
</ProgressCircle>
<ProgressCircle size={size} max={60} progress={minutes} stroke="rgb(160, 154, 40)">
<DigitBlock suffix="m">
{minutes.toLocaleString(locale, {
minimumIntegerDigits,
useGrouping,
})}
</DigitBlock>
</ProgressCircle>
<ProgressCircle size={size} max={60} progress={seconds} stroke="rgb(155, 50, 70)">
<DigitBlock suffix="s">
{durationMoment.seconds().toLocaleString(locale, {
minimumIntegerDigits,
useGrouping,
})}
</DigitBlock>
</ProgressCircle>
</div>
);
}
}

9
src/Countdown.sass Normal file
View File

@ -0,0 +1,9 @@
.countdown
display: flex
flex-direction: row
flex-wrap: wrap
font-family: monospace
justify-content: space-around
&> *:not(:first-child)
margin-left: 0.25em

22
src/DigitBlock.jsx Normal file
View File

@ -0,0 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import style from './DigitBlock.sass';
const DigitBlock = ({ children, suffix }) => (
<div className={style.digitBlock}>
<span>{children}</span>
<span className={style.digitBlockSuffix}>{suffix}</span>
</div>
);
DigitBlock.defaultProps = {
suffix: null,
};
DigitBlock.propTypes = {
children: PropTypes.node.isRequired,
suffix: PropTypes.string,
};
export default DigitBlock;

10
src/DigitBlock.sass Normal file
View File

@ -0,0 +1,10 @@
.digitBlock
font-size: 2em
display: inline-block
white-space: pre
&Suffix
font-size: 0.5em
text-transform: uppercase
font-weight: bold
color: #888

23
src/Header.jsx Normal file
View File

@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import style from './Header.sass';
export default function Header({ className, children }) {
return (
<header className={[style.header, className].join(' ')}>
{children}
</header>
);
}
Header.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
};
Header.defaultProps = {
children: null,
className: null,
};

10
src/Header.sass Normal file
View File

@ -0,0 +1,10 @@
.header
background-color: #222
color: #f8f8f8
font-weight: bold
font-size: 1.5em
text-align: center
box-shadow: 0px 4px 4vh 4px rgba(34,34,34,0.9)
z-index: 2
padding: 12px 0
width: 100%

81
src/ProgressCircle.jsx Normal file
View File

@ -0,0 +1,81 @@
import React from 'react';
import PropTypes from 'prop-types';
import style from './ProgressCircle.sass';
const ProgressCircle = ({
children,
min,
max,
progress,
size,
strokeWidth,
backStroke,
stroke,
}) => {
const width = size;
const height = size;
const viewBox = [0, 0, width, height].join(' ');
const cx = width / 2;
const cy = height / 2;
const r = (size / 2) - (strokeWidth / 2);
const strokeLength = 2 * Math.PI * r;
const finalProgress = (progress - min) / (max - min);
const strokeDasharray = strokeLength;
const strokeDashoffset = strokeLength * (1 - finalProgress);
return (
<div
className={style.progressCircleContainer}
style={{
width: `${width}px`,
height: `${height}px`,
}}
>
<svg className={style.progressCircle} {...{ width, height, viewBox }}>
<circle
fill="none"
stroke={backStroke}
{...{ cx, cy, r, strokeWidth }}
/>
<circle
className={style.progressValue}
fill="none"
{...{ cx, cy, r, stroke, strokeWidth, strokeDashoffset, strokeDasharray }}
/>
</svg>
<span className={style.progressCircleLabel}>
{children}
</span>
</div>
);
};
ProgressCircle.propTypes = {
children: PropTypes.node,
size: PropTypes.number.isRequired,
progress: PropTypes.number,
strokeWidth: PropTypes.number,
min: PropTypes.number,
max: PropTypes.number,
backStroke: PropTypes.string,
stroke: PropTypes.string,
};
ProgressCircle.defaultProps = {
stroke: '#444',
backStroke: '#eee',
strokeWidth: 10,
progress: 0.5,
min: 0,
max: 1,
children: null,
};
export default ProgressCircle;

18
src/ProgressCircle.sass Normal file
View File

@ -0,0 +1,18 @@
.progressCircle
transform: rotate(-90deg)
position: absolute
left: 0
top: 0
.progressValue
transition: stroke-dashoffset 1s linear
.progressCircleContainer
position: relative
display: inline-flex
flex-direction: column
justify-content: space-around
.progressCircleLabel
flex-basis: auto
text-align: center

16
src/getUpcomingDate.js Normal file
View File

@ -0,0 +1,16 @@
import moment from 'moment';
export default function getUpcomingDate(now, weekday: String) {
let wanted = moment()
.isoWeekday(weekday)
.second(0)
.minute(0)
.hour(0);
if (wanted.isBefore(now)) {
// we want the date from the upcoming week
wanted = wanted.add(1, 'weeks');
}
return wanted;
}

13
src/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>my-app</title>
<meta name="description" content="">
</head>
<body>
<div id="app"></div>
</body>
</html>

7
src/index.jsx Normal file
View File

@ -0,0 +1,7 @@
import React from 'react';
import { render } from 'react-dom';
import './index.sass';
import App from './App';
render(<App />, document.querySelector('#app'));

6
src/index.sass Normal file
View File

@ -0,0 +1,6 @@
@import "~normalize-scss/sass/normalize"
body
font-family: sans-serif
margin: 0
padding: 0

35
src/react.svg Normal file
View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_2_1_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 841.9 595.3" enable-background="new 0 0 841.9 595.3" xml:space="preserve">
<g>
<path fill="#61DAFB" d="M666.3,296.5c0-32.5-40.7-63.3-103.1-82.4c14.4-63.6,8-114.2-20.2-130.4c-6.5-3.8-14.1-5.6-22.4-5.6v22.3
c4.6,0,8.3,0.9,11.4,2.6c13.6,7.8,19.5,37.5,14.9,75.7c-1.1,9.4-2.9,19.3-5.1,29.4c-19.6-4.8-41-8.5-63.5-10.9
c-13.5-18.5-27.5-35.3-41.6-50c32.6-30.3,63.2-46.9,84-46.9l0-22.3c0,0,0,0,0,0c-27.5,0-63.5,19.6-99.9,53.6
c-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7,0,51.4,16.5,84,46.6c-14,14.7-28,31.4-41.3,49.9c-22.6,2.4-44,6.1-63.6,11
c-2.3-10-4-19.7-5.2-29c-4.7-38.2,1.1-67.9,14.6-75.8c3-1.8,6.9-2.6,11.5-2.6l0-22.3c0,0,0,0,0,0c-8.4,0-16,1.8-22.6,5.6
c-28.1,16.2-34.4,66.7-19.9,130.1c-62.2,19.2-102.7,49.9-102.7,82.3c0,32.5,40.7,63.3,103.1,82.4c-14.4,63.6-8,114.2,20.2,130.4
c6.5,3.8,14.1,5.6,22.5,5.6c27.5,0,63.5-19.6,99.9-53.6c36.4,33.8,72.4,53.2,99.9,53.2c8.4,0,16-1.8,22.6-5.6
c28.1-16.2,34.4-66.7,19.9-130.1C625.8,359.7,666.3,328.9,666.3,296.5z M536.1,229.8c-3.7,12.9-8.3,26.2-13.5,39.5
c-4.1-8-8.4-16-13.1-24c-4.6-8-9.5-15.8-14.4-23.4C509.3,224,523,226.6,536.1,229.8z M490.3,336.3c-7.8,13.5-15.8,26.3-24.1,38.2
c-14.9,1.3-30,2-45.2,2c-15.1,0-30.2-0.7-45-1.9c-8.3-11.9-16.4-24.6-24.2-38c-7.6-13.1-14.5-26.4-20.8-39.8
c6.2-13.4,13.2-26.8,20.7-39.9c7.8-13.5,15.8-26.3,24.1-38.2c14.9-1.3,30-2,45.2-2c15.1,0,30.2,0.7,45,1.9
c8.3,11.9,16.4,24.6,24.2,38c7.6,13.1,14.5,26.4,20.8,39.8C504.7,309.8,497.8,323.2,490.3,336.3z M522.6,323.3
c5.4,13.4,10,26.8,13.8,39.8c-13.1,3.2-26.9,5.9-41.2,8c4.9-7.7,9.8-15.6,14.4-23.7C514.2,339.4,518.5,331.3,522.6,323.3z
M421.2,430c-9.3-9.6-18.6-20.3-27.8-32c9,0.4,18.2,0.7,27.5,0.7c9.4,0,18.7-0.2,27.8-0.7C439.7,409.7,430.4,420.4,421.2,430z
M346.8,371.1c-14.2-2.1-27.9-4.7-41-7.9c3.7-12.9,8.3-26.2,13.5-39.5c4.1,8,8.4,16,13.1,24C337.1,355.7,341.9,363.5,346.8,371.1z
M420.7,163c9.3,9.6,18.6,20.3,27.8,32c-9-0.4-18.2-0.7-27.5-0.7c-9.4,0-18.7,0.2-27.8,0.7C402.2,183.3,411.5,172.6,420.7,163z
M346.7,221.9c-4.9,7.7-9.8,15.6-14.4,23.7c-4.6,8-8.9,16-13,24c-5.4-13.4-10-26.8-13.8-39.8C318.6,226.7,332.4,224,346.7,221.9z
M256.2,347.1c-35.4-15.1-58.3-34.9-58.3-50.6c0-15.7,22.9-35.6,58.3-50.6c8.6-3.7,18-7,27.7-10.1c5.7,19.6,13.2,40,22.5,60.9
c-9.2,20.8-16.6,41.1-22.2,60.6C274.3,354.2,264.9,350.8,256.2,347.1z M310,490c-13.6-7.8-19.5-37.5-14.9-75.7
c1.1-9.4,2.9-19.3,5.1-29.4c19.6,4.8,41,8.5,63.5,10.9c13.5,18.5,27.5,35.3,41.6,50c-32.6,30.3-63.2,46.9-84,46.9
C316.8,492.6,313,491.7,310,490z M547.2,413.8c4.7,38.2-1.1,67.9-14.6,75.8c-3,1.8-6.9,2.6-11.5,2.6c-20.7,0-51.4-16.5-84-46.6
c14-14.7,28-31.4,41.3-49.9c22.6-2.4,44-6.1,63.6-11C544.3,394.8,546.1,404.5,547.2,413.8z M585.7,347.1c-8.6,3.7-18,7-27.7,10.1
c-5.7-19.6-13.2-40-22.5-60.9c9.2-20.8,16.6-41.1,22.2-60.6c9.9,3.1,19.3,6.5,28.1,10.2c35.4,15.1,58.3,34.9,58.3,50.6
C644,312.2,621.1,332.1,585.7,347.1z"/>
<polygon fill="#61DAFB" points="320.8,78.4 320.8,78.4 320.8,78.4 "/>
<circle fill="#61DAFB" cx="420.9" cy="296.5" r="45.7"/>
<polygon fill="#61DAFB" points="520.5,78.1 520.5,78.1 520.5,78.1 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB