├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── README.md ├── config.js ├── deploy.config.js ├── manifest.json ├── package.json ├── server.js ├── src ├── css │ ├── index.styl │ ├── page │ │ └── SAT.styl │ └── variable.styl ├── favicon.ico ├── image │ └── favicon-144x144.png ├── index.ejs ├── index.js ├── js │ ├── actions │ │ ├── SAT.js │ │ ├── Session.js │ │ └── index.js │ ├── api │ │ ├── Base.js │ │ ├── Firebase.js │ │ └── index.js │ ├── components │ │ ├── App.jsx │ │ ├── App.styl │ │ ├── Base.jsx │ │ ├── FirebaseTest.jsx │ │ ├── Login.jsx │ │ ├── Login.styl │ │ ├── NotFound.jsx │ │ ├── SAT.jsx │ │ ├── chart.jsx │ │ ├── chart.styl │ │ ├── common │ │ │ ├── Footer.jsx │ │ │ ├── Nav.jsx │ │ │ └── index.js │ │ └── index.js │ ├── containers │ │ ├── App.js │ │ ├── Base.js │ │ ├── Chart.js │ │ ├── DevTools.js │ │ ├── FirebaseTest.js │ │ ├── Login.js │ │ ├── SAT.js │ │ ├── common │ │ │ ├── Nav.js │ │ │ └── index.js │ │ └── index.js │ ├── reducers │ │ ├── Base.js │ │ ├── SAT.js │ │ ├── Session.js │ │ └── index.js │ ├── routes.js │ ├── stores │ │ └── Store.js │ └── utils │ │ └── Base.js └── service-worker.js ├── sw-precache-config.js ├── webpack.config.js └── webpack.production.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015", 5 | "stage-2" 6 | ], 7 | "env": { 8 | "development": { 9 | "plugins": [ 10 | "transform-decorators-legacy", 11 | [ 12 | "react-transform", 13 | { 14 | "transforms": [ 15 | { 16 | "transform": "react-transform-hmr", 17 | "imports": [ 18 | "react" 19 | ], 20 | "locals": [ 21 | "module" 22 | ] 23 | }, 24 | { 25 | "transform": "react-transform-catch-errors", 26 | "imports": [ 27 | "react", 28 | "redbox-react" 29 | ] 30 | } 31 | ] 32 | } 33 | ] 34 | ] 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | node_modules/* 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | extends: "standard", 8 | plugins: ["react"], 9 | env: { 10 | 'browser': true 11 | }, 12 | rules: { 13 | "react/jsx-uses-react": "error", 14 | "react/jsx-uses-vars": "error", 15 | 'strict': 0, 16 | 'arrow-parens': 0, 17 | 'indent': [ 2, 4, { 'SwitchCase': 1 } ], 18 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | dist/ 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### React-Redux-Template 2 | --- 3 | - React 4 | - Webpack 5 | - Babel 6 | - ES6 7 | - Alt (flux implementation) 8 | 9 | ### How to use 10 | --- 11 | - npm install 12 | - npm run dev 13 | 14 | ### Production 15 | --- 16 | - npm install 17 | - npm run build 18 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | publicPath: '' 3 | } 4 | module.exports = config 5 | -------------------------------------------------------------------------------- /deploy.config.js: -------------------------------------------------------------------------------- 1 | SAT.me 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
40 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "react-pwa-boilerplate", 3 | "name": "react-pwa-boilerplate", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "144x144", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./", 12 | "display": "standalone" 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-boilerplate", 3 | "version": "1.0.0", 4 | "description": "--- - React - Webpack - Babel - ES6 - Alt (flux implementation)", 5 | "main": "index.js", 6 | "dependencies": { 7 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 8 | "bootstrap": "^3.3.7", 9 | "chart.js": "^2.4.0", 10 | "file-loader": "^0.9.0", 11 | "firebase": "^3.6.1", 12 | "history": "^4.3.0", 13 | "immutable": "^3.8.1", 14 | "js-sorting": "^3.0.4", 15 | "query-string": "^4.2.3", 16 | "react": "^15.4.1", 17 | "react-chartjs": "^0.8.0", 18 | "react-chartjs-2": "^1.5.1", 19 | "react-cookie": "^1.0.3", 20 | "react-css-modules": "^4.0.3", 21 | "react-dom": "^15.4.1", 22 | "react-facebook-login": "^3.3.3", 23 | "react-router": "^3.0.0", 24 | "react-router-redux": "^4.0.6", 25 | "reactfire": "^1.0.0", 26 | "redux-actions": "^0.12.0", 27 | "redux-promise": "^0.5.3", 28 | "redux-react-firebase": "^2.3.3", 29 | "redux-thunk": "^2.1.0", 30 | "sorted-array": "^2.0.1", 31 | "sw-precache": "^4.2.3", 32 | "sweetalert-react": "^0.4.6", 33 | "url-join": "^1.1.0", 34 | "url-loader": "^0.5.7" 35 | }, 36 | "devDependencies": { 37 | "autoprefixer": "^6.5.1", 38 | "babel-core": "^6.18.0", 39 | "babel-eslint": "^7.1.0", 40 | "babel-loader": "^6.2.7", 41 | "babel-plugin-react-transform": "^2.0.2", 42 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 43 | "babel-plugin-transform-runtime": "^6.15.0", 44 | "babel-preset-es2015": "^6.18.0", 45 | "babel-preset-react": "^6.16.0", 46 | "babel-preset-react-hmre": "^1.1.1", 47 | "babel-preset-stage-2": "^6.18.0", 48 | "babel-register": "^6.18.0", 49 | "babel-root-import": "^4.1.3", 50 | "babel-runtime": "^6.18.0", 51 | "connect-history-api-fallback": "^1.3.0", 52 | "css-loader": "^0.25.0", 53 | "eslint": "^3.8.1", 54 | "eslint-config-standard": "^6.2.1", 55 | "eslint-friendly-formatter": "^2.0.6", 56 | "eslint-loader": "^1.6.0", 57 | "eslint-plugin-html": "^1.5.5", 58 | "eslint-plugin-promise": "^3.3.0", 59 | "eslint-plugin-react": "^6.4.1", 60 | "eslint-plugin-standard": "^2.0.1", 61 | "express": "^4.14.0", 62 | "extract-text-webpack-plugin": "^1.0.1", 63 | "file": "^0.2.2", 64 | "html-webpack-plugin": "^2.24.0", 65 | "http-proxy-middleware": "^0.17.2", 66 | "node-sass": "^3.10.1", 67 | "react-transform-catch-errors": "^1.0.2", 68 | "redbox-react": "^1.3.2", 69 | "redux-devtools": "^3.3.1", 70 | "redux-devtools-dock-monitor": "^1.1.1", 71 | "redux-devtools-log-monitor": "^1.1.1", 72 | "sass-loader": "^4.0.2", 73 | "style-loader": "^0.13.1", 74 | "stylus": "^0.54.5", 75 | "stylus-loader": "^2.3.1", 76 | "webpack": "^1.13.3", 77 | "webpack-dev-middleware": "^1.8.4", 78 | "webpack-hot-middleware": "^2.13.1", 79 | "webpack-notifier": "^1.4.1" 80 | }, 81 | "scripts": { 82 | "start": "node server.js", 83 | "lint": "eslint src", 84 | "build": "NODE_ENV=production webpack -p --progress --config ./webpack.production.config.js && sw-precache --config=sw-precache-config.js", 85 | "dev": "NODE_ENV=development node server.js" 86 | }, 87 | "repository": { 88 | "type": "git", 89 | "url": "git+https://github.com/allenwhale/react-redux-boilerplate.git" 90 | }, 91 | "author": "allenwhale", 92 | "license": "MIT", 93 | "bugs": { 94 | "url": "https://github.com/allenwhale/react-redux-boilerplate/issues" 95 | }, 96 | "homepage": "https://github.com/allenwhale/react-redux-boilerplate#readme" 97 | } 98 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var express = require('express') 3 | var webpack = require('webpack') 4 | var config = require('./webpack.config') 5 | var app = express() 6 | var compiler = webpack(config) 7 | const port = process.env.PORT || 3000 8 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 9 | stats: { 10 | colors: true, 11 | hash: false, 12 | timings: true, 13 | chunks: false, 14 | chunkModules: false, 15 | modules: false 16 | }, 17 | publicPath: config.output.publicPath 18 | }) 19 | 20 | var hotMiddleware = require('webpack-hot-middleware')(compiler) 21 | compiler.plugin('compilation', function (compilation) { 22 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 23 | hotMiddleware.publish({ action: 'reload' }) 24 | cb() 25 | }) 26 | }) 27 | app.use(require('connect-history-api-fallback')()) 28 | app.use(devMiddleware) 29 | app.use(hotMiddleware) 30 | 31 | app.get('*', function (req, res) { 32 | res.sendFile(path.join(__dirname, '/src/index.html')) 33 | }) 34 | 35 | app.listen(port, '0.0.0.0', function (err) { 36 | if (err) { 37 | console.log(err) 38 | return 39 | } 40 | console.log(`Listening at http://0.0.0.0:${port}`) 41 | }) 42 | -------------------------------------------------------------------------------- /src/css/index.styl: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/earlyaccess/notosanstc.css') 2 | @import url('https://fonts.googleapis.com/css?family=Open+Sans') 3 | $black = #50514F 4 | $blue = #D8EAF9 5 | $white = #F0F0F0 6 | $fb_color = rgba(#3b5998, 0.8) 7 | $bg-color= #ff8888 //start color 8 | $stops= 100 //smoothness 9 | $time= 20s //duration of animation 10 | $hue-range= 20 //of 360deg 11 | 12 | * 13 | font-family 'Noto Sans TC', sans-serif 14 | h1.not_login_logo 15 | color $white !important 16 | font-size: 2.5em !important 17 | body 18 | margin 0px 19 | background-color $blue 20 | font-family 'Open Sans', sans-serif 21 | #app 22 | background-color $blue 23 | color $black 24 | h1 25 | font-size 36px 26 | .control-label 27 | font-size 24px 28 | font-weight 100 29 | .form-control 30 | height 50px 31 | color $black 32 | font-size 20px 33 | background transparent 34 | border-color darken($blue, 20%) 35 | border-width 2px 36 | 37 | .testFunction 38 | button 39 | padding 5px 10px 40 | margin 0 10px 41 | font-size 16px 42 | background $blue 43 | color $black 44 | border 2px solid $black 45 | border-radius 2.5px 46 | 47 | h1 48 | color $black 49 | font-weight 400 50 | font-size 3em !important 51 | 52 | h2 53 | color $black 54 | font-weight 400 55 | font-size 2.5em !important 56 | position relative 57 | padding-bottom 20px 58 | &:after 59 | content "" 60 | width 50px 61 | height 5px 62 | background-color $black 63 | border-radius 1px 64 | position absolute 65 | bottom 10px 66 | left 0 67 | 68 | .sat_btn 69 | color $black 70 | font-size 1.25em 71 | font-weight 400 72 | border 2px solid rgba(80,80,80, 0.75) 73 | border-radius 2.5px 74 | padding 5px 10px 75 | margin 10px 10px 0 0 76 | cursor pointer 77 | background transparent 78 | &:hover 79 | cursor pointer 80 | 81 | .fb_btn 82 | border 2px solid $fb_color 83 | color $fb_color 84 | .save_btn 85 | border none 86 | color: $white 87 | font-size: 1em 88 | font-weight: 400 89 | span 90 | color $fb_color 91 | .sweet-alert 92 | h2 93 | &:after 94 | display: none 95 | p 96 | max-height: 320px 97 | overflow: scroll 98 | -------------------------------------------------------------------------------- /src/css/page/SAT.styl: -------------------------------------------------------------------------------- 1 | // $black = #50514F 2 | // .form-control 3 | // height 50px 4 | // font-size 16px 5 | // background transparent 6 | // border-width 2px 7 | // color red 8 | -------------------------------------------------------------------------------- /src/css/variable.styl: -------------------------------------------------------------------------------- 1 | $black = #50514F 2 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lichin-lin/react-SAT/aeb16bfe80240aea5f7b9d06af2bce27ba77e7f3/src/favicon.ico -------------------------------------------------------------------------------- /src/image/favicon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lichin-lin/react-SAT/aeb16bfe80240aea5f7b9d06af2bce27ba77e7f3/src/image/favicon-144x144.png -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 23 | 24 | 25 |
26 | 27 | 35 | 36 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import RootRouter from './js/routes' 5 | import store from './js/stores/Store' 6 | import { chr, ord, mapArrayToObject, mapObject } from './js/utils/Base' 7 | require('bootstrap/dist/css/bootstrap.min.css') 8 | require('./css/index.styl') 9 | 10 | if (typeof (document) !== 'undefined' && window) { 11 | window.chr = chr 12 | window.ord = ord 13 | window.mapArrayToObject = mapArrayToObject 14 | window.mapObject = mapObject 15 | window.onload = () => { 16 | return render( 17 | 18 | 19 | , 20 | document.getElementById('app') 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/js/actions/SAT.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions' 2 | import Api from 'js/api' 3 | 4 | export default { 5 | getScoreData: createAction('getScoreData', Api.Firebase.getScoreData), 6 | getYearData: createAction('getYearData', Api.Firebase.getYearData), 7 | updateUserAvg: createAction('updateUserAvg', Api.Firebase.updateUserAvg), 8 | getUserTotalYearData: createAction('getUserTotalYearData', Api.Firebase.getUserTotalYearData), 9 | updateUserScore: createAction('updateUserScore', Api.Firebase.updateUserScore) 10 | } 11 | -------------------------------------------------------------------------------- /src/js/actions/Session.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions' 2 | import Api from 'js/api' 3 | export default { 4 | FBLogin: createAction('FBLogin', Api.Firebase.FBLogin), 5 | FBLogout: createAction('FBLogout', Api.Firebase.FBLogout), 6 | CookieLogin: createAction('CookieLogin', function (data) { 7 | return new Promise(function (resolve, reject) { 8 | resolve(data) 9 | }) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/js/actions/index.js: -------------------------------------------------------------------------------- 1 | import Session from './Session' 2 | import SAT from './SAT' 3 | export default { 4 | Session, 5 | SAT 6 | } 7 | -------------------------------------------------------------------------------- /src/js/api/Base.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | import qs from 'query-string' 3 | 4 | const combinePayload = (res: Promise, payload?: { [key: string]: any }) => { 5 | return Promise.resolve( 6 | res.json().then((r) => ({ 7 | ...r, 8 | payload 9 | })) 10 | ) 11 | } 12 | 13 | export default (url: string, options?: { [key: string]: any }={}) => { 14 | const URL = url 15 | let body = {} 16 | options.credentials = 'includes' 17 | if (options.method && options.method.toLowerCase() !== 'get') { 18 | body = options.body || {} 19 | } else { 20 | const element = document.createElement('a') 21 | element.href = URL 22 | body = qs.parse(element.search) 23 | } 24 | 25 | return fetch(URL, options).then((res) => { 26 | if (res.status >= 200 && res.status < 300) { 27 | return res.json() 28 | } else { 29 | return Promise.reject(combinePayload(res.json(), body)) 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /src/js/api/Firebase.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase' 2 | // import ApiFetch from './Base' 3 | // import urlJoin from 'url-join' 4 | 5 | export default { 6 | FBLogin: function (data) { 7 | var provider = new firebase.auth.FacebookAuthProvider() 8 | return firebase.auth().signInWithPopup(provider) 9 | }, 10 | FBLogout: function (data) { 11 | return firebase.auth().signOut() 12 | }, 13 | getScoreData: function (index) { 14 | var userId = firebase.auth().currentUser.uid 15 | console.log('in api, get score', userId, index) 16 | return firebase.database().ref('/users/' + userId + '/init/' + index).once('value').then(function (snapshot) { 17 | return snapshot.val() 18 | }) 19 | }, 20 | getYearData: function (index) { 21 | // let data = '1' 22 | return firebase.database().ref('/table/' + index).once('value').then(function (snapshot) { 23 | return snapshot.val() 24 | }) 25 | }, 26 | getUserTotalYearData: function () { 27 | var userId = firebase.auth().currentUser.uid 28 | console.log(userId) 29 | return firebase.database().ref('/users/' + userId).once('value').then(function (snapshot) { 30 | return snapshot.val() 31 | }) 32 | }, 33 | updateUserScore: function (path, index, data) { 34 | var userId = firebase.auth().currentUser.uid 35 | firebase.database().ref('users/' + userId + '/' + path + '/' + index).set({ 36 | Chinese: data.Chinese, 37 | English: data.English, 38 | Math: data.Math, 39 | Society: data.Society, 40 | Science: data.Science 41 | }) 42 | }, 43 | updateUserAvg: function (avg) { 44 | console.log('cout avg: ', avg) 45 | var userId = firebase.auth().currentUser.uid 46 | firebase.database().ref('users/' + userId + '/').update({ 47 | avg: avg 48 | }) 49 | }, 50 | isUserLogin: function () { 51 | var user = firebase.auth().currentUser 52 | console.log('user:', user) 53 | if (user) { 54 | console.log('From firebase api: current user: ', user) 55 | return user 56 | } else { 57 | console.log('From firebase api: no current user') 58 | return 0 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/js/api/index.js: -------------------------------------------------------------------------------- 1 | import Firebase from './Firebase' 2 | 3 | export default { 4 | Firebase 5 | } 6 | -------------------------------------------------------------------------------- /src/js/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Containers from 'js/containers' 3 | // import Components from 'js/components' 4 | import CSSModules from 'react-css-modules' 5 | 6 | export default CSSModules(class extends Component { 7 | componentDidMount () { 8 | } 9 | render () { 10 | return ( 11 |
12 | 13 |
14 | { this.props.children } 15 |
16 | { process.env.NODE_ENV !== 'production' ? : null } 17 |
18 | ) 19 | } 20 | }, require('./App.styl')) 21 | -------------------------------------------------------------------------------- /src/js/components/App.styl: -------------------------------------------------------------------------------- 1 | .app 2 | margin 0px 3 | padding-top 50px 4 | display flex 5 | min-height 100vh 6 | flex-direction column 7 | .body 8 | flex 1 9 | .margin-top 10 | margin-top: 12px 11 | .margin-bottom 12 | margin-bottom: 12px 13 | -------------------------------------------------------------------------------- /src/js/components/Base.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { 3 | Grid, 4 | Row 5 | } from 'react-bootstrap' 6 | 7 | export default class Base extends Component { 8 | static propTypes = { 9 | requiredProps: PropTypes.string.isRequired, 10 | defaultProps: PropTypes.string 11 | }; 12 | static defaultProps = { 13 | defaultProps: 'default props' 14 | }; 15 | render () { 16 | return ( 17 | 18 | 19 |
20 |

關於 學測:

21 |
22 | 大學學科能力測驗 (General Scholastic Ability Test) 23 | 是一種用於測驗高中學生, 24 | 是否具備基本的知識, 25 | 以進入台灣各大學就讀的大型考試, 26 | 由大學入學考試中心(大考中心)負責統籌舉辦。 27 |
28 |
29 |
30 | 31 |
32 |

關於 級分計算機:

33 |
34 | 這是一個單純的學測級分計算機, 35 | 此服務並非由教育部任何相關機構維護, 36 | 只是提供一個個人學習上練習的作品, 37 | 其資料也不會有其他任何用途的使用, 38 | 若是有任何疑問、建議歡迎透過以下方式聯絡: 39 | vic20087cjimlin@gmail.com 40 |
41 |
42 |
43 | 44 |
45 |

關於 本練習:

46 |
47 | 本練習使用 react-redux 框架製作而成, 48 | 將資料放在 firebase 上面並做了權限處理, 49 | 以防止資料被任意更改。未來會再加上離線功能以及手機 app 版本(主要使用 PWA), 50 | 讓使用更加方便。如果對於此作品有興趣或這是需要一些範例, 51 | 歡迎到 github 上給我個星星並使用看看,謝謝。 52 |
53 |
54 |
55 | 56 |
57 |

關於 作者:

58 |
59 | 作者目前就讀大學,平常沒事喜歡看一些設計、繪畫, 60 | 並製作些小作品給大家使用當做自我的練習, 61 | 如果對於我的作品感興趣的人歡迎到下面看看 :D 62 |
63 | 個人網頁 64 | Github 65 | 一些小設計 66 |
67 |
68 |
69 |
70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/js/components/FirebaseTest.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import { 3 | Grid, 4 | Row, 5 | Col, 6 | ListGroup 7 | } from 'react-bootstrap' 8 | // import {connect} from 'react-redux' 9 | // import {firebase, helpers} from 'redux-react-firebase' 10 | // import FacebookLogin from 'react-facebook-login' 11 | // import _ from 'lodash' 12 | // const {isLoaded, isEmpty, dataToJS} = helpers 13 | 14 | export default class extends Component { 15 | static propTypes = { 16 | FBLogin: PropTypes.func.isRequired, 17 | getUserData: PropTypes.func.isRequired, 18 | currentUser: PropTypes.object.isRequired 19 | }; 20 | static defaultProps = { 21 | }; 22 | constructor (props) { 23 | super(props) 24 | this.state = { 25 | message: '' 26 | } 27 | console.log(this.state) 28 | this.FBLogin = this.FBLogin.bind(this) 29 | this.getUserData = this.getUserData.bind(this) 30 | this.getYearData = this.getYearData.bind(this) 31 | this.onFormSubmit = this.onFormSubmit.bind(this) 32 | } 33 | onFormSubmit (event) { 34 | event.preventDefault() 35 | } 36 | FBLogin () { 37 | console.log(this.props) 38 | this.props.FBLogin() 39 | } 40 | getUserData () { 41 | console.log('get user data...') 42 | this.props.getUserData() 43 | } 44 | getYearData () { 45 | console.log('get Year data...') 46 | this.props.getYearData(97) 47 | } 48 | componentDidMount () { 49 | console.log('====here====') 50 | console.log(this.props) 51 | } 52 | componentWillMount () { 53 | console.log('---- here ----') 54 | this.props.FBLogin() 55 | this.props.getYearData(97) 56 | console.log(this.props) 57 | } 58 | render () { 59 | // current user data not load in. 60 | if (Object.keys(this.props.currentUser).length === 0) { 61 | return

Not login yet, please login

62 | } 63 | return ( 64 |
65 |
66 |
67 |
68 | 69 |
70 |
71 |
72 | 73 | 74 | 75 |
76 | 77 | 78 | 79 | {/* {window.mapObject(this.props.currentUser, (infomation) => ( 80 |
  • 81 | { infomation } 82 |
  • 83 | ))} */} 84 |
    85 | 86 |
    87 |
    88 |
    89 |
    90 | ) 91 | } 92 | } 93 | 94 | // @firebase() 95 | // class TableItem extends Component { 96 | // render () { 97 | // const {name} = this.props 98 | // // console.log(name) 99 | // return ( 100 | // 101 | //
  • 102 | //

    103 | // chniese: {name['chniese'][1]} 104 | //
    105 | // english: {name['english'][1]} 106 | //

    107 | //
  • 108 | // ) 109 | // } 110 | // } 111 | // 112 | // @firebase([ 113 | // '/table' // if list is too large you can use ['/table'] 114 | // ]) 115 | // @connect( 116 | // ({firebase}) => ({ 117 | // table: dataToJS(firebase, '/table') 118 | // }) 119 | // ) 120 | // ------------ in render ------------- 121 | // const {firebase, table} = this.props 122 | 123 | // const handleAdd = () => { 124 | // const {newTodo} = this.refs 125 | // firebase.push('/table', {text: newTodo.value, done: false}) 126 | // newTodo.value = '' 127 | // } 128 | // const tableList = (!isLoaded(table)) 129 | // ? 'Loading' : (isEmpty(table)) 130 | // ? 'Todo list is empty' : _.map(table, (name, id) => ()) 131 | 132 | // const responseFacebook = (response) => { 133 | // console.log(response) 134 | // } 135 | /* 136 | 137 | 138 | 139 | { window.mapObject(this.props.currentUser.chniese, (problem) => ( 140 |
  • 141 | {window.chr(window.ord('A') + problem.id - 1)} . {problem.title} 142 | { problem } 143 |
  • 144 | ))} 145 |
    146 | 147 |
    148 | */ 149 | -------------------------------------------------------------------------------- /src/js/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { browserHistory } from 'react-router' 3 | import SweetAlert from 'sweetalert-react' 4 | import cookie from 'react-cookie' 5 | import CSSModules from 'react-css-modules' 6 | import 'sweetalert/dist/sweetalert.css' 7 | export default CSSModules(class Base extends Component { 8 | static propTypes = { 9 | FBLogin: PropTypes.func.isRequired, 10 | currentUser: PropTypes.object.isRequired 11 | }; 12 | constructor (props) { 13 | super(props) 14 | this.FBLogin = this.FBLogin.bind(this) 15 | this.state = { 16 | fakeNum: 50, 17 | userId: '', 18 | show: false, 19 | text: `非常歡迎您光臨「學測.大平台」(以下簡稱本網站),為了讓您能夠安心使用本網站的各項服務與資訊,特此向您說明本網站的隱私權保護政策,以保障您的權益,請您詳閱下列內容: 20 |

    一、隱私權保護政策的適用範圍 21 |

    隱私權保護政策內容,包括本網站如何處理在您使用網站服務時收集到的個人識別資料。隱私權保護政策不適用於本網站以外的相關連結網站,也不適用於非本網站所委託或參與管理的人員。 22 |

    二、個人資料的蒐集、處理及利用方式 23 |

    當您造訪本網站或使用本網站所提供之功能服務時,我們將視該服務功能性質,請您提供必要的個人資料,並在該特定目的範圍內處理及利用您的個人資料;非經您書面同意,本網站不會將個人資料用於其他用途。 24 | 本網站在您使用服務信箱、問卷調查等互動性功能時,會保留您所提供的姓名、電子郵件地址、聯絡方式及使用時間等。 25 | 於一般瀏覽時,伺服器會自行記錄相關行徑,包括您使用連線設備的IP位址、使用時間、使用的瀏覽器、瀏覽及點選資料記錄等,做為我們增進網站服務的參考依據,此記錄為內部應用,決不對外公佈。 26 | 為提供精確的服務,我們會將收集的問卷調查內容進行統計與分析,分析結果之統計數據或說明文字呈現,除供內部研究外,我們會視需要公佈統計數據及說明文字,但不涉及特定個人之資料。 27 |

    三、資料之保護 28 |

    本網站主機均設有防火牆、防毒系統等相關的各項資訊安全設備及必要的安全防護措施,加以保護網站及您的個人資料採用嚴格的保護措施,只由經過授權的人員才能接觸您的個人資料,相關處理人員皆簽有保密合約,如有違反保密義務者,將會受到相關的法律處分。 29 | 如因業務需要有必要委託其他單位提供服務時,本網站亦會嚴格要求其遵守保密義務,並且採取必要檢查程序以確定其將確實遵守。 30 |

    四、網站對外的相關連結 31 |

    本網站的網頁提供其他網站的網路連結,您也可經由本網站所提供的連結,點選進入其他網站。但該連結網站不適用本網站的隱私權保護政策,您必須參考該連結網站中的隱私權保護政策。 32 |

    五、與第三人共用個人資料之政策 33 |

    本網站絕不會提供、交換、出租或出售任何您的個人資料給其他個人、團體、私人企業或公務機關,但有法律依據或合約義務者,不在此限。 34 | 前項但書之情形包括不限於: 35 | 經由您書面同意。 36 | 法律明文規定。 37 | 為免除您生命、身體、自由或財產上之危險。 38 | 與公務機關或學術研究機構合作,基於公共利益為統計或學術研究而有必要,且資料經過提供者處理或蒐集著依其揭露方式無從識別特定之當事人。 39 | 當您在網站的行為,違反服務條款或可能損害或妨礙網站與其他使用者權益或導致任何人遭受損害時,經網站管理單位研析揭露您的個人資料是為了辨識、聯絡或採取法律行動所必要者。 40 | 有利於您的權益。 41 | 本網站委託廠商協助蒐集、處理或利用您的個人資料時,將對委外廠商或個人善盡監督管理之責。 42 |

    六、Cookie之使用 43 |

    為了提供您最佳的服務,本網站會在您的電腦中放置並取用我們的Cookie,若您不願接受Cookie的寫入,您可在您使用的瀏覽器功能項中設定隱私權等級為高,即可拒絕Cookie的寫入,但可能會導至網站某些功能無法正常執行 。 44 |

    七、隱私權保護政策之修正 45 |

    本網站隱私權保護政策將因應需求隨時進行修正,修正後的條款將刊登於網站上。` 46 | } 47 | } 48 | FBLogin () { 49 | this.props.FBLogin().then((state) => { 50 | cookie.save('user', state.payload) 51 | browserHistory.push('/SAT') 52 | }) 53 | } 54 | componentWillMount () { 55 | let user = cookie.load('user') 56 | // console.log(user) 57 | if (user === undefined) { 58 | // not login yet. 59 | } else { 60 | this.props.CookieLogin(user).then(() => { 61 | browserHistory.push('/SAT') 62 | }) 63 | } 64 | } 65 | render () { 66 | return ( 67 |
    68 |
    69 |

    學測.大平台

    70 |

    歷屆試題級分計算機

    71 | 72 |
    73 |
    74 | {/*

    已有 {this.state.fakeNum} 位考生使用

    */} 75 |

    一起加入奮鬥的行列吧!

    76 | 77 |
    78 |
    79 | this.setState({ show: false })} 86 | onConfirm={() => this.setState({ show: false })} 87 | /> 88 |
    89 |
    90 | ) 91 | } 92 | }, require('./Login.styl')) 93 | -------------------------------------------------------------------------------- /src/js/components/Login.styl: -------------------------------------------------------------------------------- 1 | $black = #50514F 2 | $blue = #D8EAF9 3 | $white = #F0F0F0 4 | $fb_color = rgba(#3b5998, 0.8) 5 | $bg-color= #ff8888 //start color 6 | $stops= 100 //smoothness 7 | $time= 20s //duration of animation 8 | $hue-range= 20 //of 360deg 9 | animation_bg() 10 | background: linear-gradient(230deg,#6a67ce, #6a67ce, #6a67ce, #fc636b, #ffb900, #53d769, #53d769, #53d769) //, #ffdd00 11 | background-size: 1000% 1000% 12 | 13 | -webkit-animation: AnimationName 35s ease infinite; 14 | -moz-animation: AnimationName 35s ease infinite; 15 | animation: AnimationName 35s ease infinite; 16 | 17 | @-webkit-keyframes AnimationName { 18 | 0%{background-position:0% 2.5%} 19 | 50%{background-position:100% 97.5%} 20 | 100%{background-position:0% 2.5%} 21 | } 22 | @-moz-keyframes AnimationName { 23 | 0%{background-position:0% 2.5%} 24 | 50%{background-position:100% 97.5%} 25 | 100%{background-position:0% 2.5%} 26 | } 27 | @keyframes AnimationName { 28 | 0%{background-position:0% 2.5%} 29 | 50%{background-position:100% 97.5%} 30 | 100%{background-position:0% 2.5%} 31 | } 32 | .fb_btn 33 | color $fb_color 34 | border-color $fb_color 35 | 36 | .not_login 37 | width 100vw 38 | height 100vh 39 | animation_bg() 40 | position absolute 41 | top 0 42 | left 0 43 | z-index 9999 44 | display flex 45 | flex-direction column 46 | justify-content center 47 | align-items center 48 | .not_login_title 49 | text-align center 50 | position absolute 51 | bottom 10px 52 | left 0px 53 | width 100% 54 | color $white !important 55 | p 56 | letter-spacing 2px 57 | color $white !important 58 | .not_login_section 59 | max-width 480px 60 | text-align center 61 | position relative 62 | p 63 | color $white 64 | font-size 1.5em 65 | 66 | 67 | .info_section 68 | margin 20px 0 69 | padding 0 20px 70 | .info_content 71 | letter-spacing 2px 72 | font-size 1.5em 73 | color $black 74 | .web 75 | margin-right 10px 76 | .star 77 | color rgba(#fcb314, 0.75) 78 | a 79 | color $black 80 | position relative 81 | text-decoration none 82 | font-size 0.75em 83 | cursor pointer 84 | &:after 85 | content "" 86 | width 100% 87 | height 5px 88 | background-color rgba($fb_color, 0.35) 89 | border-radius 1px 90 | position absolute 91 | bottom 0px 92 | left 0 93 | -------------------------------------------------------------------------------- /src/js/components/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | export default class NotFound extends Component { 4 | 5 | render () { 6 | return ( 7 |
    8 | NotFound 9 |
    10 | ) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/js/components/SAT.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | // import reactDOM from 'react-dom' 3 | import { 4 | Grid, 5 | FormGroup, 6 | ControlLabel, 7 | FormControl 8 | } from 'react-bootstrap' 9 | require('./../../css/page/SAT.styl') 10 | export default class extends Component { 11 | static propTypes = { 12 | getScoreData: PropTypes.func.isRequired, 13 | getYearData: PropTypes.func.isRequired, 14 | updateUserScore: PropTypes.func.isRequired, 15 | userData: PropTypes.object.isRequired 16 | }; 17 | static defaultProps = { 18 | }; 19 | constructor (props) { 20 | super(props) 21 | this.state = { 22 | message: '', 23 | selectedYear: 96, 24 | scoreResult: '', 25 | Chinese: 0, 26 | English: 0, 27 | Math: 0, 28 | Society: 0, 29 | Science: 0 30 | } 31 | this.printResult = this.printResult.bind(this) 32 | this.onFormSubmit = this.onFormSubmit.bind(this) 33 | this.handleSelectChange = this.handleSelectChange.bind(this) 34 | this.handleInputChange = this.handleInputChange.bind(this) 35 | } 36 | onFormSubmit (event) { 37 | event.preventDefault() 38 | var mappingTable = ['chniese', 'english', 'math', 'science', 'social'] 39 | var mappingValue = [15, 15, 15, 15, 15] 40 | var OriYear = 'SAT' + (1911 + parseInt(this.state.selectedYear)) 41 | var OriData = { 42 | Chinese: parseInt(this.state.Chinese), // parseInt(reactDOM.findDOMNode(this.refs.Chinese).value), 43 | English: parseInt(this.state.English), // parseInt(reactDOM.findDOMNode(this.refs.English).value), 44 | Math: parseInt(this.state.Math), // parseInt(reactDOM.findDOMNode(this.refs.Math).value), 45 | Science: parseInt(this.state.Science), 46 | Society: parseInt(this.state.Society) 47 | } 48 | this.props.updateUserScore('init', OriYear, OriData) 49 | 50 | // trans to score. 51 | var calcData = OriData 52 | let mIndex = 0 53 | for (var key in OriData) { 54 | // skip loop if the property is from prototype 55 | if (!OriData.hasOwnProperty(key)) continue 56 | // get Original score, then do transfer. 57 | var score = OriData[key] 58 | // console.log(this.props) 59 | var compareList = this.props.userData.MeasureScore[mappingTable[mIndex]] 60 | 61 | if (score === 0) { 62 | mappingValue[mIndex] = 0 63 | } else { 64 | // console.log('here=======', compareList) 65 | for (let k = 0; k <= 15; k++) { 66 | if (score < compareList[k]) { 67 | mappingValue[mIndex] = k 68 | break 69 | } 70 | } 71 | } 72 | mIndex += 1 73 | } 74 | // update Object then ready to send back; 75 | mIndex = 0 76 | for (var calckey in calcData) { 77 | if (!calcData.hasOwnProperty(calckey)) continue 78 | calcData[calckey] = mappingValue[mIndex] 79 | mIndex += 1 80 | } 81 | // print in result input box 82 | // write back to user`s databse 83 | this.printResult(mappingValue) 84 | this.props.updateUserScore('score', OriYear, calcData) 85 | } 86 | 87 | printResult (mappingValue) { 88 | var newScoreResult = '' 89 | for (let i = 0; i < 5; i++) { 90 | newScoreResult += mappingValue[i] 91 | if (i < 4) newScoreResult += ' / ' 92 | } 93 | this.setState({scoreResult: newScoreResult}) 94 | } 95 | 96 | handleSelectChange (event) { 97 | var selectedYear = event.target.value 98 | this.setState({selectedYear: selectedYear}) 99 | var yearIndex = 'SAT' + (1911 + parseInt(selectedYear)) 100 | this.props.getYearData(yearIndex) 101 | this.props.getScoreData(yearIndex) 102 | } 103 | 104 | handleInputChange (event) { 105 | var tmp = {} 106 | tmp[event.target.name] = event.target.value 107 | this.setState(tmp) 108 | } 109 | 110 | componentWillReceiveProps (nextProps) { 111 | // console.log('receive', nextProps) 112 | this.setState({ 113 | ...nextProps.userData.StudentScore 114 | }) 115 | } 116 | 117 | componentDidMount () { 118 | } 119 | 120 | componentWillMount () { 121 | } 122 | render () { 123 | return ( 124 |
    125 | 126 |
    127 |

    轉換 {this.state.selectedYear} 年原始分數

    128 | 129 | 選擇年份 choose year 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 國文 Chinese 145 | 146 | 147 | 148 | 英文 English 149 | 150 | 151 | 152 | 數學 Math 153 | 154 | 155 | 156 | 自然 Science 157 | 158 | 159 | 160 | 社會 Society 161 | 162 | 163 | 164 |

    165 | 166 | 轉換級分結果 167 | 168 | 169 | 170 |
    171 |

    172 |
    173 |
    174 | ) 175 | } 176 | } 177 | 178 | // icon , name 179 | // chart, with total score 180 | // setting, style 181 | -------------------------------------------------------------------------------- /src/js/components/chart.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import _ from 'lodash' 3 | // import { mergeSort } from 'js-sorting' 4 | import { 5 | Line 6 | } from 'react-chartjs-2' 7 | import { 8 | Grid, 9 | Row, 10 | Col 11 | } from 'react-bootstrap' 12 | import CSSModules from 'react-css-modules' 13 | export default CSSModules(class extends Component { 14 | static propTypes = { 15 | FBLogin: PropTypes.func.isRequired, 16 | getYearData: PropTypes.func.isRequired, 17 | // updateUserAvg: PropTypes.func.isRequired, 18 | getUserTotalYearData: PropTypes.func.isRequired, 19 | userData: PropTypes.object.isRequired, 20 | currentUser: PropTypes.object.isRequired 21 | }; 22 | constructor (props) { 23 | super(props) 24 | this.state = { 25 | counter: 0, 26 | fakeNum: 50, 27 | avg: 0, 28 | height: 400, 29 | width: 400, 30 | totalChartData: { 31 | labels: ['96', '97', '98', '99', '100', '101', '102', '103', '104', '105'], 32 | datasets: [ 33 | { 34 | borderWidth: 2, 35 | backgroundColor: 'rgba(164,211,250,0.25)', 36 | borderColor: 'rgba(164,211,250,1)', 37 | pointColor: 'rgba(164,211,250,1)', 38 | pointStrokeColor: '#fff', 39 | pointHighlightFill: '#fff', 40 | pointHighlightStroke: 'rgba(220,220,220,1)', 41 | data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 42 | 43 | } 44 | ] 45 | }, 46 | singleChartData: { 47 | labels: ['96', '97', '98', '99', '100', '101', '102', '103', '104', '105'], 48 | datasets: [ 49 | { 50 | label: 'Chinese', 51 | borderWidth: 2, 52 | backgroundColor: 'rgba(255,90,100,0.05)', 53 | borderColor: 'rgba(255,90,100,1)', 54 | pointColor: 'rgba(255,90,100,1)', 55 | pointStrokeColor: '#fff', 56 | pointHighlightFill: '#fff', 57 | pointHighlightStroke: 'rgba(220,220,220,1)', 58 | data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 59 | }, 60 | { 61 | label: 'English', 62 | borderWidth: 2, 63 | backgroundColor: 'rgba(46,135,170,0.05)', 64 | borderColor: 'rgba(46,135,170,1)', 65 | pointColor: 'rgba(46,135,170,1)', 66 | pointStrokeColor: '#fff', 67 | pointHighlightFill: '#fff', 68 | pointHighlightStroke: 'rgba(220,220,220,1)', 69 | data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 70 | }, 71 | { 72 | label: 'Math', 73 | borderWidth: 2, 74 | backgroundColor: 'rgba(245,210,95,0.05)', 75 | borderColor: 'rgba(245,210,95,1)', 76 | pointColor: 'rgba(245,210,95,1)', 77 | pointStrokeColor: '#fff', 78 | pointHighlightFill: '#fff', 79 | pointHighlightStroke: 'rgba(220,220,220,1)', 80 | data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 81 | }, 82 | { 83 | label: 'Society', 84 | borderWidth: 2, 85 | backgroundColor: 'rgba(105,150,90,0.05)', 86 | borderColor: 'rgba(105,150,90,1)', 87 | pointColor: 'rgba(105,150,90,1)', 88 | pointStrokeColor: '#fff', 89 | pointHighlightFill: '#fff', 90 | pointHighlightStroke: 'rgba(220,220,220,1)', 91 | data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 92 | }, 93 | { 94 | label: 'Science', 95 | borderWidth: 2, 96 | backgroundColor: 'rgba(80,80,80,0.05)', 97 | borderColor: 'rgba(80,80,80,1)', 98 | pointColor: 'rgba(80,80,80,1)', 99 | pointStrokeColor: '#fff', 100 | pointHighlightFill: '#fff', 101 | pointHighlightStroke: 'rgba(220,220,220,1)', 102 | data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 103 | } 104 | ] 105 | }, 106 | chartOptions: { 107 | title: { 108 | display: false, 109 | text: 'SAT score every year / 年份對照' 110 | }, 111 | legend: { 112 | display: false 113 | } 114 | // responsive: true, 115 | // maintainAspectRatio: true, 116 | // bezierCurveTension: 0.25, 117 | // scaleOverride: true, 118 | // // scaleSteps: 5, 119 | // // scaleStepWidth: 15, 120 | // scaleStartValue: 0, 121 | // legendTemplate: '' 122 | }, 123 | singleChartOptions: { 124 | legend: { 125 | position: 'bottom', 126 | labels: { 127 | boxWidth: 12 128 | } 129 | } 130 | // responsive: true, 131 | // maintainAspectRatio: true, 132 | // bezierCurveTension: 0.25, 133 | // scaleOverride: true, 134 | // scaleSteps: 15, 135 | // scaleStepWidth: 1, 136 | // scaleStartValue: 0, 137 | // legendTemplate: '' 138 | } 139 | } 140 | this.changedata = this.changedata.bind(this) 141 | this.updateUserAvg = this.updateUserAvg.bind(this) 142 | this.getUserTotalYearData = this.getUserTotalYearData.bind(this) 143 | this.updateChartSize = this.updateChartSize.bind(this) 144 | } 145 | 146 | changedata () { 147 | let totalDataArray = new Array(10) 148 | let singleDataArray = new Array(5) 149 | // init single data 150 | for (let i = 0; i < 5; i++) { 151 | singleDataArray[i] = new Array(10) 152 | for (let j = 0; j < 10; j++) { 153 | singleDataArray[i][j] = 0 154 | } 155 | } 156 | // init avg params 157 | var avgCounter = 0 158 | var avgNum = 0 159 | // init total data 160 | for (let i = 0; i < 10; i++) { 161 | totalDataArray[i] = 0 162 | } 163 | var chartDataObj = this.props.userData.TotalScore.score 164 | for (var key in chartDataObj) { 165 | var yearTotalScore = 0 166 | // SAT2007 -> 2007 -> 0 167 | var mappingIndex = parseInt(key.slice(3)) - 2007 168 | let subjectCount = 0 169 | for (var subject in chartDataObj[key]) { 170 | // console.log('index: ', mappingIndex, ', sub: ', subjectCount, 'score: ', chartDataObj[key][subject]) 171 | yearTotalScore += chartDataObj[key][subject] 172 | singleDataArray[subjectCount][mappingIndex] = chartDataObj[key][subject] 173 | subjectCount += 1 174 | } 175 | totalDataArray[mappingIndex] = yearTotalScore 176 | if (yearTotalScore > 0) { 177 | avgCounter += 1 178 | avgNum += yearTotalScore 179 | } 180 | } 181 | this.props.updateUserAvg(avgNum / avgCounter) 182 | this.setState({avg: (avgNum / avgCounter).toFixed(2)}) 183 | 184 | // put total data back to state. 185 | let newArray = _.extend({}, this.state.totalChartData) 186 | newArray.datasets[0].data = totalDataArray 187 | this.setState({totalChartData: newArray}) 188 | 189 | // put single data back to state. 190 | for (let i = 0; i < 5; i++) { 191 | let newArray = _.extend({}, this.state.singleChartData) 192 | newArray.datasets[i].data = singleDataArray[i] 193 | this.setState({singleChartData: newArray}) 194 | } 195 | this.setState({counter: this.state.counter + 1}) 196 | } 197 | updateChartSize () { 198 | var w = window 199 | var d = document 200 | var documentElement = d.documentElement 201 | var body = d.getElementsByTagName('body')[0] 202 | var width = w.innerWidth || documentElement.clientWidth || body.clientWidth 203 | width *= 0.9 204 | var height = width * 3 / 4 205 | // console.log('resize', width, height) 206 | this.setState({width: width, height: height}) 207 | // console.log('yo', this.state.width, this.state.height) 208 | } 209 | getUserTotalYearData () { 210 | this.props.getUserTotalYearData() 211 | } 212 | updateUserAvg () { 213 | this.props.updateUserAvg() 214 | } 215 | componentWillMount () { 216 | this.props.getUserTotalYearData() 217 | // var tmp = mergeSort([5, 3, 2, 4, 1, 7, 4, 10]) 218 | // var l = tmp.length 219 | // var input = 7 220 | // var result = l 221 | // for (var i = 0; i < l; i++) { 222 | // if (input < tmp[i]) { 223 | // result = i 224 | // break 225 | // } 226 | // } 227 | } 228 | componentDidMount () { 229 | window.addEventListener('resize', this.updateChartSize) 230 | } 231 | render () { 232 | return ( 233 |
    234 | 235 | 236 | 237 |

    你好,{this.props.currentUser.AuthData.user.displayName}

    238 | 239 |
    240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 |

    總覽

    249 | 250 | 251 | 252 |

    單科

    253 | 254 | 255 | 256 |

    平均

    257 |
    {this.state.avg}
    258 | 259 | 260 |

    排名

    261 |
    功能尚未推出,請耐心等待
    262 | 263 |
    264 |
    265 |
    266 | ) 267 | } 268 | }, require('./chart.styl')) 269 | 270 | // icon , name 271 | // chart, with total score 272 | // setting, style 273 | -------------------------------------------------------------------------------- /src/js/components/chart.styl: -------------------------------------------------------------------------------- 1 | @import "./App.styl" 2 | $black = #50514F 3 | $blue = #D8EAF9 4 | $white = #F0F0F0 5 | $fb_color = rgba(#3b5998, 0.8) 6 | $bg-color= #ff8888 //start color 7 | $stops= 100 //smoothness 8 | $time= 20s //duration of animation 9 | $hue-range= 20 //of 360deg 10 | .chart_card_contain 11 | padding 0 15px 12 | .chart_card 13 | padding 5px 15px 10px 14 | margin 15px 0 15 | border-radius 5px 16 | background white 17 | box-shadow 3px 4px 5px rgba($black, 0.25) 18 | .chart_titleText 19 | font-size 2.5em 20 | .chart_contentText 21 | font-size 1.25em 22 | -------------------------------------------------------------------------------- /src/js/components/common/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | export default class Footer extends Component { 4 | render () { 5 | return ( 6 | 9 | ) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/js/components/common/Nav.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { 3 | Navbar, 4 | NavItem, 5 | Nav 6 | } from 'react-bootstrap' 7 | import { IndexLinkContainer, LinkContainer } from 'react-router-bootstrap' 8 | export default class extends Component { 9 | render () { 10 | return ( 11 | 12 | 13 | 14 | 15 | 學測.大平台 16 | 17 | 18 | 19 | 20 | 21 | 32 | 33 | 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/js/components/common/index.js: -------------------------------------------------------------------------------- 1 | import Nav from './Nav' 2 | import Footer from './Footer' 3 | export default { 4 | Nav, 5 | Footer 6 | } 7 | -------------------------------------------------------------------------------- /src/js/components/index.js: -------------------------------------------------------------------------------- 1 | import common from './common' 2 | import App from './App' 3 | import Base from './Base' 4 | import Chart from './Chart' 5 | import FirebaseTest from './FirebaseTest' 6 | import Login from './Login' 7 | import NotFound from './NotFound' 8 | import SAT from './SAT' 9 | export default { 10 | common, 11 | App, 12 | Base, 13 | Chart, 14 | FirebaseTest, 15 | Login, 16 | NotFound, 17 | SAT 18 | } 19 | -------------------------------------------------------------------------------- /src/js/containers/App.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Components from 'js/components' 3 | import Action from 'js/actions' 4 | 5 | const mapStateToProps = (state) => ({ 6 | }) 7 | const mapDispatchToProps = (dispatch) => ({ 8 | FBLogout: (data) => dispatch(Action.Session.FBLogout(data)), 9 | FBLogin: (data) => dispatch(Action.Session.FBLogin(data)), 10 | getUserData: () => dispatch(Action.User.getUserData()) 11 | }) 12 | export default connect(mapStateToProps, mapDispatchToProps)(Components.App) 13 | -------------------------------------------------------------------------------- /src/js/containers/Base.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Components from 'js/components' 3 | 4 | const mapStateToProps = (state, ownProps) => ({ 5 | ...ownProps 6 | }) 7 | 8 | const mapDispatchToProps = (dispatch) => ({ 9 | }) 10 | 11 | export default connect(mapStateToProps, mapDispatchToProps)(Components.Base) 12 | -------------------------------------------------------------------------------- /src/js/containers/Chart.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Components from 'js/components' 3 | import Action from 'js/actions' 4 | const mapStateToProps = (state) => ({ 5 | currentUser: state.Session, 6 | userData: state.SAT 7 | }) 8 | 9 | const mapDispatchToProps = (dispatch) => ({ 10 | FBLogin: () => dispatch(Action.Session.FBLogin()), 11 | isUserLogin: () => dispatch(Action.Session.isUserLogin()), 12 | getYearData: (year) => dispatch(Action.SAT.getYearData(year)), 13 | updateUserAvg: (avg) => dispatch(Action.SAT.updateUserAvg(avg)), 14 | getUserTotalYearData: () => dispatch(Action.SAT.getUserTotalYearData()) 15 | }) 16 | 17 | export default connect(mapStateToProps, mapDispatchToProps)(Components.Chart) 18 | -------------------------------------------------------------------------------- /src/js/containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createDevTools } from 'redux-devtools' 3 | import LogMonitor from 'redux-devtools-log-monitor' 4 | import DockMonitor from 'redux-devtools-dock-monitor' 5 | 6 | export default createDevTools( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /src/js/containers/FirebaseTest.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Components from 'js/components' 3 | import Action from 'js/actions' 4 | const mapStateToProps = (state) => ({ 5 | currentUser: state.Session, 6 | userData: state.Score 7 | }) 8 | 9 | const mapDispatchToProps = (dispatch) => ({ 10 | FBLogout: () => dispatch(Action.Session.FBLogout()), 11 | FBLogin: () => dispatch(Action.Session.FBLogin()), 12 | getUserData: () => dispatch(Action.Score.getUserData()), 13 | getYearData: (year) => dispatch(Action.Score.getYearData(year)) 14 | }) 15 | 16 | export default connect(mapStateToProps, mapDispatchToProps)(Components.FirebaseTest) 17 | -------------------------------------------------------------------------------- /src/js/containers/Login.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Components from 'js/components' 3 | import Action from 'js/actions' 4 | const mapStateToProps = (state) => ({ 5 | currentUser: state.Session 6 | }) 7 | 8 | const mapDispatchToProps = (dispatch) => ({ 9 | FBLogin: () => dispatch(Action.Session.FBLogin()), 10 | CookieLogin: (data) => dispatch(Action.Session.CookieLogin(data)) 11 | }) 12 | 13 | export default connect(mapStateToProps, mapDispatchToProps)(Components.Login) 14 | -------------------------------------------------------------------------------- /src/js/containers/SAT.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Components from 'js/components' 3 | import Action from 'js/actions' 4 | const mapStateToProps = (state) => ({ 5 | userData: state.SAT 6 | }) 7 | 8 | const mapDispatchToProps = (dispatch) => ({ 9 | getScoreData: (year) => dispatch(Action.SAT.getScoreData(year)), 10 | getYearData: (year) => dispatch(Action.SAT.getYearData(year)), 11 | updateUserScore: (path, index, data) => dispatch(Action.SAT.updateUserScore(path, index, data)) 12 | }) 13 | 14 | export default connect(mapStateToProps, mapDispatchToProps)(Components.SAT) 15 | -------------------------------------------------------------------------------- /src/js/containers/common/Nav.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Components from 'js/components' 3 | 4 | const mapStateToProps = (store) => ({}) 5 | const mapDispatchToProps = (dispatch) => ({}) 6 | 7 | export default connect(mapStateToProps, mapDispatchToProps)(Components.common.Nav) 8 | -------------------------------------------------------------------------------- /src/js/containers/common/index.js: -------------------------------------------------------------------------------- 1 | import Nav from './Nav' 2 | 3 | export default { 4 | Nav 5 | } 6 | -------------------------------------------------------------------------------- /src/js/containers/index.js: -------------------------------------------------------------------------------- 1 | import common from './common' 2 | import App from './App' 3 | import Base from './Base' 4 | import Chart from './Chart' 5 | import DevTools from './DevTools' 6 | import FirebaseTest from './FirebaseTest' 7 | import Login from './Login' 8 | import SAT from './SAT' 9 | export default { 10 | common, 11 | App, 12 | Base, 13 | Chart, 14 | DevTools, 15 | FirebaseTest, 16 | Login, 17 | SAT 18 | } 19 | -------------------------------------------------------------------------------- /src/js/reducers/Base.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions' 2 | 3 | const initialState = {} 4 | 5 | export default handleActions({ 6 | 7 | ACTION: { 8 | next (state, action) { 9 | return { 10 | ...state 11 | } 12 | }, 13 | throw (state, action) { 14 | return { 15 | ...state 16 | } 17 | } 18 | }, 19 | 20 | default: (state, action) => { 21 | return { 22 | ...state 23 | } 24 | } 25 | }, initialState) 26 | -------------------------------------------------------------------------------- /src/js/reducers/SAT.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions' 2 | 3 | const initialState = { 4 | MeasureScore: {}, 5 | StudentScore: {}, 6 | TotalScore: {} 7 | } 8 | 9 | export default handleActions({ 10 | 11 | getScoreData: { 12 | next (state, action) { 13 | console.log('get score', action.payload) 14 | return { 15 | ...state, 16 | StudentScore: action.payload 17 | } 18 | }, 19 | throw (state, action) { 20 | return { 21 | ...state 22 | } 23 | } 24 | }, 25 | 26 | getYearData: { 27 | next (state, action) { 28 | console.log('get year', action.payload) 29 | return { 30 | ...state, 31 | MeasureScore: action.payload 32 | } 33 | }, 34 | throw (state, action) { 35 | return { 36 | ...state 37 | } 38 | } 39 | }, 40 | 41 | getUserTotalYearData: { 42 | next (state, action) { 43 | console.log(action.payload) 44 | return { 45 | ...state, 46 | TotalScore: action.payload 47 | } 48 | }, 49 | throw (state, action) { 50 | return { 51 | ...state 52 | } 53 | } 54 | }, 55 | 56 | updateUserScore: { 57 | next (state, action) { 58 | return { 59 | ...state, 60 | ...action.payload 61 | } 62 | }, 63 | throw (state, action) { 64 | return { 65 | ...state 66 | } 67 | } 68 | }, 69 | 70 | updateUserAvg: { 71 | next (state, action) { 72 | return { 73 | ...state, 74 | ...action.payload 75 | } 76 | }, 77 | throw (state, action) { 78 | return { 79 | ...state 80 | } 81 | } 82 | }, 83 | 84 | default: (state, action) => { 85 | return { 86 | ...state 87 | } 88 | } 89 | }, initialState) 90 | -------------------------------------------------------------------------------- /src/js/reducers/Session.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions' 2 | 3 | const initialState = { 4 | AuthData: {} 5 | } 6 | 7 | export default handleActions({ 8 | 9 | FBLogin: { 10 | next (state, action) { 11 | return { 12 | ...state, 13 | AuthData: action.payload 14 | } 15 | }, 16 | throw (state, action) { 17 | return { 18 | AuthData: {} 19 | } 20 | } 21 | }, 22 | 23 | FBLogout: { 24 | next (state, action) { 25 | return { 26 | ...state, 27 | AuthData: action.payload 28 | } 29 | }, 30 | throw (state, action) { 31 | return { 32 | ...state 33 | } 34 | } 35 | }, 36 | 37 | CookieLogin: { 38 | next (state, action) { 39 | return { 40 | ...state, 41 | AuthData: action.payload 42 | } 43 | } 44 | }, 45 | 46 | default: (state, action) => { 47 | return { 48 | ...state 49 | } 50 | } 51 | }, initialState) 52 | -------------------------------------------------------------------------------- /src/js/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { routerReducer as routing } from 'react-router-redux' 3 | import { firebaseStateReducer } from 'redux-react-firebase' 4 | import base from './Base' 5 | import Session from './Session' 6 | import SAT from './SAT' 7 | 8 | export default combineReducers({ 9 | base, 10 | Session, 11 | SAT, 12 | firebase: firebaseStateReducer, 13 | routing 14 | }) 15 | -------------------------------------------------------------------------------- /src/js/routes.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Router, Route, IndexRoute, browserHistory } from 'react-router' 3 | import store from './stores/Store' 4 | import { syncHistoryWithStore } from 'react-router-redux' 5 | const history = syncHistoryWithStore(browserHistory, store) 6 | 7 | // import Components from 'js/components' 8 | import Containers from 'js/containers' 9 | 10 | const checkLogin = (next) => { 11 | if (Object.keys(store.getState().Session.AuthData).length === 0) { 12 | browserHistory.push('/login') 13 | } 14 | } 15 | 16 | export default class Root extends Component { 17 | render () { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/js/stores/Store.js: -------------------------------------------------------------------------------- 1 | // import { createStore, compose, applyMiddleware } from 'redux' 2 | import { createStore, compose, applyMiddleware } from 'redux' 3 | import {reduxReactFirebase} from 'redux-react-firebase' 4 | import thunk from 'redux-thunk' 5 | import promiseMiddleware from 'redux-promise' 6 | import rootReducer from '../reducers' 7 | import DevTools from '../containers/DevTools' 8 | 9 | function configureStore () { 10 | const middleware = [thunk, promiseMiddleware] 11 | 12 | const config = { 13 | apiKey: 'AIzaSyDBaa3EU9oHINKc4iVzqG8TfIcKvoUk3KM', 14 | authDomain: 'sat-transtable.firebaseapp.com', 15 | databaseURL: 'https://sat-transtable.firebaseio.com', 16 | storageBucket: 'sat-transtable.appspot.com', 17 | messagingSenderId: '564068329530' 18 | } 19 | 20 | const finalCreateStore = compose( 21 | reduxReactFirebase(config), 22 | applyMiddleware(...middleware), 23 | DevTools.instrument(), 24 | window.devToolsExtensio ? window.devToolsExtension() : f => f 25 | )(createStore) 26 | 27 | let store = finalCreateStore(rootReducer) 28 | 29 | if (module.hot) { 30 | module.hot.accept('../reducers', () => { 31 | const nextRootReducer = require('../reducers') 32 | store.replaceReducer(nextRootReducer) 33 | }) 34 | } 35 | 36 | return store 37 | } 38 | 39 | export default configureStore() 40 | -------------------------------------------------------------------------------- /src/js/utils/Base.js: -------------------------------------------------------------------------------- 1 | export const mapArrayToObject = (arr: Array, indexName: string='id'): {[key: string]: any} => { 2 | let res = {} 3 | arr.map(x => (res[x[indexName]] = x)) 4 | return res 5 | } 6 | 7 | export const mapObject = (self: Object, func: Function, reverse?: Boolean=false): Array => { 8 | let keys = Object.keys(self) 9 | let res = [] 10 | let cnt = 0 11 | if (reverse) { 12 | keys = keys.reverse() 13 | } 14 | for (let i in keys) { 15 | let key = keys[i] 16 | res.push(func(self[key], cnt++, key)) 17 | } 18 | return res 19 | } 20 | 21 | export const ord = x => x.charCodeAt(0) 22 | 23 | export const chr = x => String.fromCharCode(x) 24 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // DO NOT EDIT THIS GENERATED OUTPUT DIRECTLY! 18 | // This file should be overwritten as part of your build process. 19 | // If you need to extend the behavior of the generated service worker, the best approach is to write 20 | // additional code and include it using the importScripts option: 21 | // https://github.com/GoogleChrome/sw-precache#importscripts-arraystring 22 | // 23 | // Alternatively, it's possible to make changes to the underlying template file and then use that as the 24 | // new base for generating output, via the templateFilePath option: 25 | // https://github.com/GoogleChrome/sw-precache#templatefilepath-string 26 | // 27 | // If you go that route, make sure that whenever you update your sw-precache dependency, you reconcile any 28 | // changes made to this original template file with your modified copy. 29 | 30 | // This generated service worker JavaScript will precache your site's resources. 31 | // The code needs to be saved in a .js file at the top-level of your site, and registered 32 | // from your pages in order to be used. See 33 | // https://github.com/googlechrome/sw-precache/blob/master/demo/app/js/service-worker-registration.js 34 | // for an example of how you can register this script and handle various service worker events. 35 | 36 | /* eslint-env worker, serviceworker */ 37 | /* eslint-disable indent, no-unused-vars, no-multiple-empty-lines, max-nested-callbacks, space-before-function-paren, quotes, comma-spacing */ 38 | 'use strict'; 39 | 40 | var precacheConfig = [["bundle.03694cc6ffca538d4049.js","ea650fef44e11f3c7471f7b62ac1d9bc"],["bundle.7bde41c824889d33ebe4.js","b3aa2eb9923199866e89bbd19de8bbcc"],["index.ejs","065632a885294a8d94dae9e18a7811e4"],["manifest.json","4cf2749166febe3fa4fb849b9d30aec8"],["styles.44866b09a5dac73caf02.css","dddd73f39ce7a2849e11c14a4a705b12"],["styles.c72501f4bdf42b11fedd.css","dddd73f39ce7a2849e11c14a4a705b12"],["vendor.05fded41e40a8db773dd.js","23319b79553ebaf3ab9347ab132cde08"],["vendor.78eb30390eb14406b7d6.js","92eb0a43f13be1d260b5e532d45cd39a"]]; 41 | var cacheName = 'sw-precache-v2-sw-precache-' + (self.registration ? self.registration.scope : ''); 42 | 43 | 44 | var ignoreUrlParametersMatching = [/^utm_/]; 45 | 46 | 47 | 48 | var addDirectoryIndex = function (originalUrl, index) { 49 | var url = new URL(originalUrl); 50 | if (url.pathname.slice(-1) === '/') { 51 | url.pathname += index; 52 | } 53 | return url.toString(); 54 | }; 55 | 56 | var createCacheKey = function (originalUrl, paramName, paramValue, 57 | dontCacheBustUrlsMatching) { 58 | // Create a new URL object to avoid modifying originalUrl. 59 | var url = new URL(originalUrl); 60 | 61 | // If dontCacheBustUrlsMatching is not set, or if we don't have a match, 62 | // then add in the extra cache-busting URL parameter. 63 | if (!dontCacheBustUrlsMatching || 64 | !(url.toString().match(dontCacheBustUrlsMatching))) { 65 | url.search += (url.search ? '&' : '') + 66 | encodeURIComponent(paramName) + '=' + encodeURIComponent(paramValue); 67 | } 68 | 69 | return url.toString(); 70 | }; 71 | 72 | var isPathWhitelisted = function (whitelist, absoluteUrlString) { 73 | // If the whitelist is empty, then consider all URLs to be whitelisted. 74 | if (whitelist.length === 0) { 75 | return true; 76 | } 77 | 78 | // Otherwise compare each path regex to the path of the URL passed in. 79 | var path = (new URL(absoluteUrlString)).pathname; 80 | return whitelist.some(function(whitelistedPathRegex) { 81 | return path.match(whitelistedPathRegex); 82 | }); 83 | }; 84 | 85 | var stripIgnoredUrlParameters = function (originalUrl, 86 | ignoreUrlParametersMatching) { 87 | var url = new URL(originalUrl); 88 | 89 | url.search = url.search.slice(1) // Exclude initial '?' 90 | .split('&') // Split into an array of 'key=value' strings 91 | .map(function(kv) { 92 | return kv.split('='); // Split each 'key=value' string into a [key, value] array 93 | }) 94 | .filter(function(kv) { 95 | return ignoreUrlParametersMatching.every(function(ignoredRegex) { 96 | return !ignoredRegex.test(kv[0]); // Return true iff the key doesn't match any of the regexes. 97 | }); 98 | }) 99 | .map(function(kv) { 100 | return kv.join('='); // Join each [key, value] array into a 'key=value' string 101 | }) 102 | .join('&'); // Join the array of 'key=value' strings into a string with '&' in between each 103 | 104 | return url.toString(); 105 | }; 106 | 107 | 108 | var hashParamName = '_sw-precache'; 109 | var urlsToCacheKeys = new Map( 110 | precacheConfig.map(function(item) { 111 | var relativeUrl = item[0]; 112 | var hash = item[1]; 113 | var absoluteUrl = new URL(relativeUrl, self.location); 114 | var cacheKey = createCacheKey(absoluteUrl, hashParamName, hash, /\.\w{8}\./); 115 | return [absoluteUrl.toString(), cacheKey]; 116 | }) 117 | ); 118 | 119 | function setOfCachedUrls(cache) { 120 | return cache.keys().then(function(requests) { 121 | return requests.map(function(request) { 122 | return request.url; 123 | }); 124 | }).then(function(urls) { 125 | return new Set(urls); 126 | }); 127 | } 128 | 129 | self.addEventListener('install', function(event) { 130 | event.waitUntil( 131 | caches.open(cacheName).then(function(cache) { 132 | return setOfCachedUrls(cache).then(function(cachedUrls) { 133 | return Promise.all( 134 | Array.from(urlsToCacheKeys.values()).map(function(cacheKey) { 135 | // If we don't have a key matching url in the cache already, add it. 136 | if (!cachedUrls.has(cacheKey)) { 137 | return cache.add(new Request(cacheKey, {credentials: 'same-origin'})); 138 | } 139 | }) 140 | ); 141 | }); 142 | }).then(function() { 143 | 144 | // Force the SW to transition from installing -> active state 145 | return self.skipWaiting(); 146 | 147 | }) 148 | ); 149 | }); 150 | 151 | self.addEventListener('activate', function(event) { 152 | var setOfExpectedUrls = new Set(urlsToCacheKeys.values()); 153 | 154 | event.waitUntil( 155 | caches.open(cacheName).then(function(cache) { 156 | return cache.keys().then(function(existingRequests) { 157 | return Promise.all( 158 | existingRequests.map(function(existingRequest) { 159 | if (!setOfExpectedUrls.has(existingRequest.url)) { 160 | return cache.delete(existingRequest); 161 | } 162 | }) 163 | ); 164 | }); 165 | }).then(function() { 166 | 167 | return self.clients.claim(); 168 | 169 | }) 170 | ); 171 | }); 172 | 173 | 174 | self.addEventListener('fetch', function(event) { 175 | if (event.request.method === 'GET') { 176 | // Should we call event.respondWith() inside this fetch event handler? 177 | // This needs to be determined synchronously, which will give other fetch 178 | // handlers a chance to handle the request if need be. 179 | var shouldRespond; 180 | 181 | // First, remove all the ignored parameter and see if we have that URL 182 | // in our cache. If so, great! shouldRespond will be true. 183 | var url = stripIgnoredUrlParameters(event.request.url, ignoreUrlParametersMatching); 184 | shouldRespond = urlsToCacheKeys.has(url); 185 | 186 | // If shouldRespond is false, check again, this time with 'index.html' 187 | // (or whatever the directoryIndex option is set to) at the end. 188 | var directoryIndex = 'index.ejs'; 189 | if (!shouldRespond && directoryIndex) { 190 | url = addDirectoryIndex(url, directoryIndex); 191 | shouldRespond = urlsToCacheKeys.has(url); 192 | } 193 | 194 | // If shouldRespond is still false, check to see if this is a navigation 195 | // request, and if so, whether the URL matches navigateFallbackWhitelist. 196 | var navigateFallback = ''; 197 | if (!shouldRespond && 198 | navigateFallback && 199 | (event.request.mode === 'navigate') && 200 | isPathWhitelisted([], event.request.url)) { 201 | url = new URL(navigateFallback, self.location).toString(); 202 | shouldRespond = urlsToCacheKeys.has(url); 203 | } 204 | 205 | // If shouldRespond was set to true at any point, then call 206 | // event.respondWith(), using the appropriate cache key. 207 | if (shouldRespond) { 208 | event.respondWith( 209 | caches.open(cacheName).then(function(cache) { 210 | return cache.match(urlsToCacheKeys.get(url)).then(function(response) { 211 | if (response) { 212 | return response; 213 | } 214 | throw Error('The cached response that was expected is missing.'); 215 | }); 216 | }).catch(function(e) { 217 | // Fall back to just fetch()ing the request if some unexpected error 218 | // prevented the cached response from being valid. 219 | console.warn('Couldn\'t serve response for "%s" from cache: %O', event.request.url, e); 220 | return fetch(event.request); 221 | }) 222 | ); 223 | } 224 | } 225 | }); 226 | -------------------------------------------------------------------------------- /sw-precache-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stripPrefix: 'dist/', 3 | staticFileGlobs: [ 4 | 'dist/*.html', 5 | 'dist/manifest.json', 6 | 'dist/static/**/!(*map*)', 7 | 'dist/*.css', 8 | 'dist/*.js' 9 | ], 10 | dontCacheBustUrlsMatching: /\.\w{8}\./, 11 | swFilePath: 'dist/service-worker.js' 12 | }; 13 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | var HtmlWebpackPlugin = require('html-webpack-plugin') 4 | module.exports = { 5 | devtool: 'cheap-module-eval-source-map', 6 | devServer: { 7 | historyApiFallback: true 8 | }, 9 | entry: [ 10 | 'webpack-hot-middleware/client', 11 | './src/index.js' 12 | ], 13 | output: { 14 | path: path.join(__dirname, 'dist'), 15 | filename: 'bundle.js', 16 | publicPath: '/' 17 | }, 18 | resolve: { 19 | extensions: ['', '.js', '.jsx'], 20 | alias: { 21 | 'src': path.join(__dirname, './src'), 22 | 'containers': path.join(__dirname, './src/js/containers'), 23 | 'components': path.join(__dirname, './src/js/components'), 24 | 'js': path.join(__dirname, './src/js') 25 | } 26 | }, 27 | module: { 28 | preLoaders: [ 29 | { 30 | test: /\.jsx?$/, 31 | loader: 'eslint-loader', 32 | include: path.join(__dirname, 'src/'), 33 | exclude: /node_modules/ 34 | } 35 | ], 36 | loaders: [ 37 | { 38 | test: /\.(jsx|js)$/, 39 | exclude: /node_modules/, 40 | loaders: ['babel-loader'], 41 | include: path.join(__dirname, 'src/') 42 | }, 43 | { 44 | test: /\.sass$/, loader: 'style!css!sass' 45 | }, 46 | { 47 | test: /\.css$/, loader: 'style!css' 48 | }, 49 | { 50 | test: /\.styl$/, 51 | loader: 'style-loader!css-loader!stylus-loader' 52 | }, 53 | { 54 | test: /\.json$/, loader: 'json' 55 | }, 56 | { 57 | test: /\.(ttf|eot|png|gif|jpg|woff|woff2|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 58 | loader: 'url-loader?limit=8192' 59 | }, 60 | { 61 | test: /\.(html|png)$/, 62 | loader: 'file?name=[path][name].[ext]&context=./src' 63 | } 64 | ] 65 | }, 66 | eslint: { 67 | formatter: require('eslint-friendly-formatter') 68 | }, 69 | plugins: [ 70 | new webpack.DefinePlugin({ 71 | 'process.env.NODE_ENV': '"development"' 72 | }), 73 | new webpack.NoErrorsPlugin(), 74 | new webpack.HotModuleReplacementPlugin(), 75 | new HtmlWebpackPlugin({ 76 | template: 'src/index.ejs', 77 | inject: true 78 | }) 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /webpack.production.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var path = require('path') 3 | var webpack = require('webpack') 4 | var HtmlWebpackPlugin = require('html-webpack-plugin') 5 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 6 | 7 | module.exports = { 8 | entry: { 9 | bundle: [ 10 | path.resolve(__dirname, 'src/index.js') 11 | ] 12 | }, 13 | output: { 14 | path: path.resolve(__dirname, 'dist'), 15 | filename: '[name].[chunkhash].js', 16 | chunkFilename: '[id].[chunkhash].js', 17 | publicPath: require('./config.js').publicPath 18 | }, 19 | debug: false, 20 | devtool: false, 21 | stats: { 22 | colors: true, 23 | reasons: false, 24 | progress: true 25 | }, 26 | resolve: { 27 | extensions: ['', '.js', '.jsx'], 28 | alias: { 29 | 'src': path.join(__dirname, './src'), 30 | 'containers': path.join(__dirname, './src/js/containers'), 31 | 'components': path.join(__dirname, './src/js/components'), 32 | 'js': path.join(__dirname, './src/js') 33 | } 34 | }, 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.(jsx|js)$/, 39 | exclude: /node_modules/, 40 | loader: 'babel-loader', 41 | noParse: '/node_modules/', 42 | include: path.join(__dirname, 'src/') 43 | }, 44 | { 45 | test: /\.sass$/, 46 | loader: ExtractTextPlugin.extract( 47 | 'style-loader', 48 | 'css-loader!sass-loader' 49 | ) 50 | }, 51 | { 52 | test: /\.styl$/, 53 | loader: ExtractTextPlugin.extract( 54 | 'style-loader', 55 | 'css-loader!stylus-loader' 56 | ) 57 | }, 58 | { 59 | test: /\.css$/, 60 | loader: ExtractTextPlugin.extract( 61 | 'style-loader', 62 | 'css-loader' 63 | ) 64 | }, 65 | { 66 | test: /\.json$/, 67 | loader: 'json' 68 | }, 69 | { 70 | test: /\.(ttf|eot|png|gif|jpg|woff|woff2|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 71 | loader: 'url-loader?limit=8192' 72 | }, 73 | { 74 | test: /\.(html|png)$/, 75 | loader: 'file?name=[path][name].[ext]&context=./src' 76 | } 77 | ] 78 | }, 79 | plugins: [ 80 | new webpack.optimize.CommonsChunkPlugin({ 81 | name: 'vendor', 82 | minChunks: function (module, count) { 83 | return ( 84 | module.resource && 85 | /\.js$/.test(module.resource) && 86 | module.resource.indexOf( 87 | path.join(__dirname, 'node_modules') 88 | ) === 0 89 | ) 90 | } 91 | }), 92 | new ExtractTextPlugin('styles.[hash].css'), 93 | new webpack.DefinePlugin({ 94 | 'process.env': { 95 | 'NODE_ENV': JSON.stringify('production') 96 | } 97 | }), 98 | new webpack.optimize.DedupePlugin(), 99 | new webpack.ProvidePlugin({ 100 | Promise: 'bluebird' 101 | }), 102 | new webpack.optimize.UglifyJsPlugin({ 103 | compress: { 104 | warnings: false, 105 | properties: true, 106 | sequences: true, 107 | dead_code: true, 108 | drop_console: true, 109 | conditionals: true, 110 | comparisons: true, 111 | evaluate: true, 112 | booleans: true, 113 | unused: true, 114 | loops: true, 115 | hoist_funs: true, 116 | cascade: true, 117 | if_return: true, 118 | join_vars: true, 119 | drop_debugger: true, 120 | unsafe: true, 121 | hoist_vars: true, 122 | negate_iife: true 123 | }, 124 | comments: false, 125 | mangle: true, 126 | minimize: true 127 | }), 128 | new webpack.optimize.OccurenceOrderPlugin(), 129 | new webpack.optimize.AggressiveMergingPlugin(), 130 | new HtmlWebpackPlugin({ 131 | template: 'src/index.ejs', 132 | inject: 'body', 133 | minify: { 134 | removeComments: true, 135 | collapseWhitespace: true, 136 | collapseInlineTagWhitspace: true, 137 | removeAttributeQuotes: true 138 | }, 139 | chunksSortMode: 'dependency' 140 | }) 141 | ] 142 | } 143 | --------------------------------------------------------------------------------