Browse code
Initial commit
fiddlerwoaroof authored on 11/03/2017 23:02:22
Showing 9 changed files
Showing 9 changed files
- .babelrc
- .gitignore
- README.md
- index.html
- package.json
- src/App.vue
- src/assets/logo.png
- src/main.js
- webpack.config.js
0 | 6 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,18 @@ |
1 |
+# time-tracker |
|
2 |
+ |
|
3 |
+> A Vue.js project |
|
4 |
+ |
|
5 |
+## Build Setup |
|
6 |
+ |
|
7 |
+``` bash |
|
8 |
+# install dependencies |
|
9 |
+npm install |
|
10 |
+ |
|
11 |
+# serve with hot reload at localhost:8080 |
|
12 |
+npm run dev |
|
13 |
+ |
|
14 |
+# build for production with minification |
|
15 |
+npm run build |
|
16 |
+``` |
|
17 |
+ |
|
18 |
+For detailed explanation on how things work, consult the [docs for vue-loader](http://vuejs.github.io/vue-loader). |
0 | 12 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,29 @@ |
1 |
+{ |
|
2 |
+ "name": "time-tracker", |
|
3 |
+ "description": "A Vue.js project", |
|
4 |
+ "version": "1.0.0", |
|
5 |
+ "author": "fiddlerwoaroof <fiddlerwoaroof@gmail.com>", |
|
6 |
+ "private": true, |
|
7 |
+ "scripts": { |
|
8 |
+ "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot --host 0.0.0.0", |
|
9 |
+ "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" |
|
10 |
+ }, |
|
11 |
+ "dependencies": { |
|
12 |
+ "vue": "^2.2.1", |
|
13 |
+ "vuex": "^2.2.1" |
|
14 |
+ }, |
|
15 |
+ "devDependencies": { |
|
16 |
+ "babel-core": "^6.0.0", |
|
17 |
+ "babel-loader": "^6.0.0", |
|
18 |
+ "babel-preset-latest": "^6.0.0", |
|
19 |
+ "cross-env": "^3.0.0", |
|
20 |
+ "css-loader": "^0.25.0", |
|
21 |
+ "file-loader": "^0.9.0", |
|
22 |
+ "node-sass": "^4.5.0", |
|
23 |
+ "sass-loader": "^5.0.1", |
|
24 |
+ "vue-loader": "^11.1.4", |
|
25 |
+ "vue-template-compiler": "^2.2.1", |
|
26 |
+ "webpack": "^2.2.0", |
|
27 |
+ "webpack-dev-server": "^2.2.0" |
|
28 |
+ } |
|
29 |
+} |
0 | 30 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,213 @@ |
1 |
+<template> |
|
2 |
+ <div id="app"> |
|
3 |
+ <header> |
|
4 |
+ <h1> |
|
5 |
+ Timer |
|
6 |
+ </h1> |
|
7 |
+ <div> |
|
8 |
+ <button @click="mark">Go</button> |
|
9 |
+ </div> |
|
10 |
+ </header> |
|
11 |
+ <ol> |
|
12 |
+ <li class=header> |
|
13 |
+ <div>Time</div><div>Lap Time</div><div>Cumulative</div> |
|
14 |
+ </li> |
|
15 |
+ |
|
16 |
+ <li v-for="[time,diff,cum,new_annotation] in times"> |
|
17 |
+ <div class="time">{{formatTime(time.time)}}</div> |
|
18 |
+ <div class="diff"><div class="time">{{formatDiff(diff)[0]}}</div><div class="mills">.{{formatDiff(diff)[1]}}</div></div> |
|
19 |
+ <div class="cum"><div class="time">{{formatDiff(cum)[0]}}</div><div class="mills">.{{formatDiff(cum)[1]}}</div></div> |
|
20 |
+ <div class="actions"> |
|
21 |
+ <form @submit.prevent="annotate(time.id, new_annotation)"> |
|
22 |
+ <input type=text v-model="new_annotation"> |
|
23 |
+ <button type="submit">+</button> |
|
24 |
+ </form> |
|
25 |
+ </div> |
|
26 |
+ <ul v-show="time.annotations.length > 0"> |
|
27 |
+ <li class="annotation" v-for="annotation in time.annotations"> |
|
28 |
+ <div> |
|
29 |
+ {{annotation.text}} |
|
30 |
+ </div> |
|
31 |
+ <time datetime="annotation.time">{{formatTime(annotation.time)}}</time> |
|
32 |
+ </li> |
|
33 |
+ </ul> |
|
34 |
+ |
|
35 |
+ </li> |
|
36 |
+ </ol> |
|
37 |
+ </div> |
|
38 |
+ </template> |
|
39 |
+ |
|
40 |
+<script> |
|
41 |
+ |
|
42 |
+import {mapGetters} from 'vuex'; |
|
43 |
+ |
|
44 |
+export default { |
|
45 |
+ name: 'app', |
|
46 |
+ data () { |
|
47 |
+ return { |
|
48 |
+ msg: 'Welcome to Your Vue.js App', |
|
49 |
+ } |
|
50 |
+ }, |
|
51 |
+ |
|
52 |
+ computed: { |
|
53 |
+ times() { |
|
54 |
+ let result = []; |
|
55 |
+ for (let cur of this.$store.state.times) { |
|
56 |
+ let diff = 0, cum = 0; |
|
57 |
+ if (cur.id > 0) { |
|
58 |
+ cum = cur.time - result[cur.id-1][0].time; |
|
59 |
+ diff = cur.time-result[0][0].time |
|
60 |
+ } |
|
61 |
+ |
|
62 |
+ result.unshift([cur, diff, cum, '']); |
|
63 |
+ } |
|
64 |
+ return result; |
|
65 |
+ }, |
|
66 |
+ }, |
|
67 |
+ |
|
68 |
+ methods: { |
|
69 |
+ annotate(idx, annotation) { |
|
70 |
+ this.$store.commit('annotate', {idx, annotation}); |
|
71 |
+ }, |
|
72 |
+ mark() { |
|
73 |
+ this.$store.commit('add'); |
|
74 |
+ }, |
|
75 |
+ |
|
76 |
+ formatTime: function (time) { |
|
77 |
+ return (new Intl.DateTimeFormat('en-US', { |
|
78 |
+ year:'numeric', month:'numeric', day:'numeric', hour:'2-digit', minute: '2-digit', second:'2-digit' |
|
79 |
+ })).format(time); |
|
80 |
+ }, |
|
81 |
+ |
|
82 |
+ formatDiff(diff) { |
|
83 |
+ let secs = Math.floor(diff/1000), |
|
84 |
+ mills = diff % 1000, |
|
85 |
+ mins = Math.floor(secs/60); |
|
86 |
+ secs = secs % 60; |
|
87 |
+ mins = `${mins}`; |
|
88 |
+ if (mills.length < 3) { |
|
89 |
+ let pad = '0'*(3-mills.length); |
|
90 |
+ mills = pad + mills; |
|
91 |
+ } |
|
92 |
+ let result = `${secs}` |
|
93 |
+ if (mins > 0) { |
|
94 |
+ result = `${mins}:${result.padStart(2,'0')}`; |
|
95 |
+ } |
|
96 |
+ return [result,mills]; |
|
97 |
+ } |
|
98 |
+ }, |
|
99 |
+} |
|
100 |
+ </script> |
|
101 |
+ |
|
102 |
+<style lang="scss"> |
|
103 |
+ * { box-sizing: border-box; } |
|
104 |
+ #app { |
|
105 |
+ font-family: 'Lato', Helvetica, Arial, sans-serif; |
|
106 |
+ -webkit-font-smoothing: antialiased; |
|
107 |
+ -moz-osx-font-smoothing: grayscale; |
|
108 |
+ text-align: right; |
|
109 |
+ color: #2c3e50; |
|
110 |
+ } |
|
111 |
+ |
|
112 |
+ header { |
|
113 |
+ display: grid; |
|
114 |
+ grid-template-columns: 75fr 25fr; |
|
115 |
+ height: 3em; |
|
116 |
+ line-height: 3em; |
|
117 |
+ padding: 0; |
|
118 |
+ } |
|
119 |
+ |
|
120 |
+ h1, h2 { |
|
121 |
+ text-align: center; |
|
122 |
+ font-size: 1.25em; |
|
123 |
+ padding: 0; |
|
124 |
+ margin: 0; |
|
125 |
+ } |
|
126 |
+ |
|
127 |
+ ul { |
|
128 |
+ list-style-type: none; |
|
129 |
+ padding: 0; |
|
130 |
+ } |
|
131 |
+ |
|
132 |
+ a { |
|
133 |
+ color: #42b983; |
|
134 |
+ } |
|
135 |
+ |
|
136 |
+ ol { |
|
137 |
+ position: absolute; |
|
138 |
+ width: 100%; |
|
139 |
+ top: 4em; |
|
140 |
+ bottom: 0; |
|
141 |
+ left: 0; |
|
142 |
+ margin: 0; |
|
143 |
+ padding: 0 1em; |
|
144 |
+ padding-top: 3.25em; |
|
145 |
+ overflow-y: scroll; |
|
146 |
+ overflow-x: auto; |
|
147 |
+ } |
|
148 |
+ |
|
149 |
+ ol > li { |
|
150 |
+ display: grid; |
|
151 |
+ grid-template-columns: 25fr 12.5fr 12.5fr 25fr 25fr; |
|
152 |
+ padding: 0.5em; |
|
153 |
+ margin-bottom: 0.25em; |
|
154 |
+ } |
|
155 |
+ |
|
156 |
+ li.header { |
|
157 |
+ position: fixed; |
|
158 |
+ top: 4em; |
|
159 |
+ left: 1em; |
|
160 |
+ right: 1em; |
|
161 |
+ background: #eee; |
|
162 |
+ } |
|
163 |
+ |
|
164 |
+ ul { |
|
165 |
+ /* grid-column-start: 2; */ |
|
166 |
+ /* grid-column-end: span 2; */ |
|
167 |
+ padding: 0.25em; |
|
168 |
+ margin: 0.25em; |
|
169 |
+ padding-right: 0; |
|
170 |
+ margin-right: 0; |
|
171 |
+ } |
|
172 |
+ ul > li { |
|
173 |
+ padding: 0.2em 1em; |
|
174 |
+ background: #ffb; |
|
175 |
+ font-style: italic; |
|
176 |
+ } |
|
177 |
+ ul > li+li { |
|
178 |
+ border-top: 0.25em double #eee; |
|
179 |
+ } |
|
180 |
+ |
|
181 |
+ .diff, .cum { |
|
182 |
+ text-align: right; |
|
183 |
+ } |
|
184 |
+ |
|
185 |
+ .diff div, .cum div { |
|
186 |
+ display: inline-block; |
|
187 |
+ } |
|
188 |
+ |
|
189 |
+ .diff .time, .cum .time { |
|
190 |
+ max-width: 80%; |
|
191 |
+ text-align: right; |
|
192 |
+ } |
|
193 |
+ |
|
194 |
+ .diff .mills, .cum .mills { |
|
195 |
+ width: 20%; |
|
196 |
+ text-align: left; |
|
197 |
+ } |
|
198 |
+ |
|
199 |
+ button { |
|
200 |
+ height: 100%; |
|
201 |
+ } |
|
202 |
+ |
|
203 |
+ .annotation { |
|
204 |
+ color: #888; |
|
205 |
+ } |
|
206 |
+ .annotation time::before { |
|
207 |
+ content: "— " |
|
208 |
+ } |
|
209 |
+ .annotation time { |
|
210 |
+ font-size: 60%; |
|
211 |
+ |
|
212 |
+ } |
|
213 |
+</style> |
2 | 216 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,23 @@ |
1 |
+import Vue from 'vue'; |
|
2 |
+import Vuex from 'vuex'; |
|
3 |
+import App from './App.vue'; |
|
4 |
+Vue.use(Vuex); |
|
5 |
+ |
|
6 |
+new Vue({ |
|
7 |
+ el: '#app', |
|
8 |
+ render: h => h(App), |
|
9 |
+ store: new Vuex.Store({ |
|
10 |
+ state: { |
|
11 |
+ times: [] |
|
12 |
+ }, |
|
13 |
+ mutations: { |
|
14 |
+ add ({times}) { |
|
15 |
+ times.push({id: times.length, time: new Date(), annotations: []}); |
|
16 |
+ }, |
|
17 |
+ |
|
18 |
+ annotate({times}, {idx, annotation}) { |
|
19 |
+ times[idx].annotations.unshift({time: new Date(), text: annotation}); |
|
20 |
+ } |
|
21 |
+ } |
|
22 |
+ }) |
|
23 |
+}) |
0 | 24 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,75 @@ |
1 |
+var path = require('path') |
|
2 |
+var webpack = require('webpack') |
|
3 |
+ |
|
4 |
+module.exports = { |
|
5 |
+ entry: './src/main.js', |
|
6 |
+ output: { |
|
7 |
+ path: path.resolve(__dirname, './dist'), |
|
8 |
+ publicPath: '/dist/', |
|
9 |
+ filename: 'build.js' |
|
10 |
+ }, |
|
11 |
+ module: { |
|
12 |
+ rules: [ |
|
13 |
+ { |
|
14 |
+ test: /\.vue$/, |
|
15 |
+ loader: 'vue-loader', |
|
16 |
+ options: { |
|
17 |
+ loaders: { |
|
18 |
+ // Since sass-loader (weirdly) has SCSS as its default parse mode, we map |
|
19 |
+ // the "scss" and "sass" values for the lang attribute to the right configs here. |
|
20 |
+ // other preprocessors should work out of the box, no loader config like this necessary. |
|
21 |
+ 'scss': 'vue-style-loader!css-loader!sass-loader', |
|
22 |
+ 'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax' |
|
23 |
+ } |
|
24 |
+ // other vue-loader options go here |
|
25 |
+ } |
|
26 |
+ }, |
|
27 |
+ { |
|
28 |
+ test: /\.js$/, |
|
29 |
+ loader: 'babel-loader', |
|
30 |
+ exclude: /node_modules/ |
|
31 |
+ }, |
|
32 |
+ { |
|
33 |
+ test: /\.(png|jpg|gif|svg)$/, |
|
34 |
+ loader: 'file-loader', |
|
35 |
+ options: { |
|
36 |
+ name: '[name].[ext]?[hash]' |
|
37 |
+ } |
|
38 |
+ } |
|
39 |
+ ] |
|
40 |
+ }, |
|
41 |
+ resolve: { |
|
42 |
+ alias: { |
|
43 |
+ 'vue$': 'vue/dist/vue.esm.js' |
|
44 |
+ } |
|
45 |
+ }, |
|
46 |
+ devServer: { |
|
47 |
+ historyApiFallback: true, |
|
48 |
+ noInfo: true |
|
49 |
+ }, |
|
50 |
+ performance: { |
|
51 |
+ hints: false |
|
52 |
+ }, |
|
53 |
+ devtool: '#eval-source-map' |
|
54 |
+} |
|
55 |
+ |
|
56 |
+if (process.env.NODE_ENV === 'production') { |
|
57 |
+ module.exports.devtool = '#source-map' |
|
58 |
+ // http://vue-loader.vuejs.org/en/workflow/production.html |
|
59 |
+ module.exports.plugins = (module.exports.plugins || []).concat([ |
|
60 |
+ new webpack.DefinePlugin({ |
|
61 |
+ 'process.env': { |
|
62 |
+ NODE_ENV: '"production"' |
|
63 |
+ } |
|
64 |
+ }), |
|
65 |
+ new webpack.optimize.UglifyJsPlugin({ |
|
66 |
+ sourceMap: true, |
|
67 |
+ compress: { |
|
68 |
+ warnings: false |
|
69 |
+ } |
|
70 |
+ }), |
|
71 |
+ new webpack.LoaderOptionsPlugin({ |
|
72 |
+ minimize: true |
|
73 |
+ }) |
|
74 |
+ ]) |
|
75 |
+} |