├── .gitignore
├── LICENSE
├── README.md
├── docs
├── index.html
├── script.js
└── style.css
├── index.html
├── package.json
├── script.es5.js
├── script.js
└── style.scss
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /style.css
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Reminder App
2 | Just prototyping with CSS animations & transitions
3 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Reminder App
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
21 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {{ tooltipText }}
33 |
34 |
35 |
36 |
75 |
125 |
126 | Your browser doesn't support speech synthesis.
127 |
128 |
129 |
130 |
131 | {{ time.value }}
132 |
133 |
134 |
135 |
153 |
154 |
155 |
156 | {{ percentsLeft }}
157 |
158 |
%
159 |
160 |
161 |
164 | {{ percentsLeft > 0 ? activeReminder.buttonTxt : 'Reset' }}
165 |
166 |
167 |
168 |
169 |
--------------------------------------------------------------------------------
/docs/script.js:
--------------------------------------------------------------------------------
1 | var SpeechRecognition = SpeechRecognition || window.webkitSpeechRecognition || undefined;
2 | var numbers = Array.apply(null, Array(101)).map(function (_, i) {return i;});
3 |
4 | if(SpeechRecognition) {
5 | var SpeechGrammarList = SpeechGrammarList || window.webkitSpeechGrammarList || undefined;
6 | var SpeechRecognitionEvent = SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent || undefined;
7 |
8 | var commands = ['reset', 'timer' ].concat( numbers);
9 | var grammar = '#JSGF V1.0; grammar colors; public = ' + commands.join(' | ') + ' ;'
10 |
11 | var speechRecognitionList = new SpeechGrammarList();
12 | speechRecognitionList.addFromString(grammar, 1);
13 |
14 | var recognition = new SpeechRecognition();
15 | recognition.grammars = speechRecognitionList;
16 | //recognition.continuous = false;
17 | recognition.lang = 'en-US';
18 | recognition.interimResults = true;
19 | recognition.maxAlternatives = 1;
20 | }
21 |
22 | var speechSynth = new SpeechSynthesisUtterance();
23 |
24 | var padDigits = function (number, digits) {
25 | return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number;
26 | }
27 |
28 | var calculatePercentsLeft = function (value, from) {
29 | return Math.floor(Math.ceil(value/1000) / (from * 60) * 100)
30 | }
31 |
32 | var calculateScaleFactor = function (percent) {
33 | return 1-(100-percent)/100;
34 | }
35 |
36 | function guid() {
37 | function s4() {
38 | return Math.floor((1 + Math.random()) * 0x10000)
39 | .toString(16)
40 | .substring(1);
41 | }
42 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
43 | s4() + '-' + s4() + s4() + s4();
44 | }
45 |
46 | var settings = {
47 | water: {
48 | warningMsg: 'Remember to drink',
49 | timeIsUpMsg: 'Time\'s up. You really need to drink now',
50 | buttonTxt: 'Drink',
51 | waveFrontColor: '#32BAFA',
52 | waveBackColor: '#2C7FBE',
53 | stageBg: '#1E384C',
54 | durationInMinutes: 1
55 | },
56 | coffee: {
57 | warningMsg: 'It\'s almost coffee time.',
58 | timeIsUpMsg: 'Time\'s up. Let\'s take a coffee break!',
59 | buttonTxt: 'Drink coffee',
60 | waveFrontColor: '#b39374',
61 | waveBackColor: '#7a6057',
62 | stageBg: '#392a2c',
63 | durationInMinutes: 2
64 | },
65 | break: {
66 | warningMsg: 'It is time to rest your eyes soon!',
67 | timeIsUpMsg: 'Time\'s up. Now, it\'s really time to rest your eyes!',
68 | buttonTxt: 'Take a break',
69 | waveFrontColor: '#02C39A',
70 | waveBackColor: '#028090',
71 | stageBg: '#012F35',
72 | durationInMinutes: 1
73 | }
74 | };
75 |
76 | new Vue({
77 | el: '#stage',
78 | data: function data() {
79 | return {
80 | color: '',
81 | percents: [100],
82 | percentsLeft: 100,
83 | secondsLeft: 0,
84 | waveStyles: '',
85 | duration: 1,
86 | timer: [],
87 | voicesOpen: false,
88 | voices: [],
89 | selectedVoice: {},
90 | countdownObj: {},
91 | activeReminder: settings.water,
92 | menuOpen: false,
93 | isListening: false,
94 | tooltipText: 'Say eg. "reset"',
95 | stageBg: settings.water.stageBg
96 | }
97 | },
98 | mounted: function mounted() {
99 | var this$1 = this;
100 |
101 | this.resetTimer();
102 | this.voices = speechSynthesis.getVoices();
103 |
104 | if(this.voices.length === 0) {
105 | speechSynthesis.onvoiceschanged = function () {
106 | this$1.voices = speechSynthesis.getVoices();
107 | };
108 | }
109 | },
110 | computed: {
111 | supportSpeechSynth: function supportSpeechSynth() {
112 | return 'speechSynthesis' in window;
113 | },
114 | supportSpeechRecognition: function supportSpeechRecognition() {
115 | return SpeechRecognition;
116 | }
117 | },
118 | watch: {
119 | percentsLeft: function(val, oldVal) {
120 | if (val === oldVal) {
121 | return;
122 | }
123 | this.percents.splice(0, 1);
124 | this.percents.push(val);
125 | }
126 | },
127 | methods: {
128 | setActiveReminder: function setActiveReminder(reminder) {
129 | this.activeReminder = settings[reminder];
130 | this.stageBg = this.activeReminder.stageBg;
131 | },
132 | toggleMenu: function toggleMenu() {
133 | this.menuOpen = !this.menuOpen;
134 | if(this.menuOpen) {
135 | this.pauseTimer();
136 | this.waveStyles = "transform: translate3d(0,100%,0); transition-delay: .25s;";
137 | }else {
138 | this.continueTimer();
139 | }
140 | },
141 | toggleVoicesMenu: function toggleVoicesMenu() {
142 | this.voicesOpen = !this.voicesOpen;
143 | },
144 | voiceSelected: function voiceSelected(voice) {
145 | this.selectedVoice = voice;
146 | speechSynth.voice = voice;
147 | },
148 | start: function start(reminder) {
149 | this.setActiveReminder(reminder);
150 | this.percents = [100];
151 | this.timer = [];
152 | this.menuOpen = false;
153 | this.resetTimer();
154 | },
155 | resetTimer: function resetTimer() {
156 | var durationInSeconds = 60 * this.activeReminder.durationInMinutes;
157 | this.startTimer(durationInSeconds);
158 | },
159 | startTimer: function startTimer(secondsLeft) {
160 | var this$1 = this;
161 |
162 | var now = new Date();
163 |
164 | // later on, this timer may be stopped
165 | if(this.countdown) {
166 | window.clearInterval(this.countdown);
167 | }
168 |
169 | this.countdown = countdown(function (ts) {
170 | this$1.secondsLeft= Math.ceil(ts.value/1000);
171 | this$1.percentsLeft = calculatePercentsLeft(ts.value,this$1.activeReminder.durationInMinutes);
172 | this$1.waveStyles = "transform: scale(1," + (calculateScaleFactor(this$1.percentsLeft)) + ")";
173 | this$1.updateCountdown(ts);
174 | if(this$1.percentsLeft == 10) {
175 | this$1.giveWarning();
176 | }
177 | if(this$1.percentsLeft <= 0){
178 | this$1.timeIsUpMessage();
179 | this$1.pauseTimer();
180 | this$1.timer = [];
181 | setTimeout(function () {
182 | this$1.startListenVoiceCommands();
183 | }, 1500);
184 |
185 | }
186 | }, now.getTime() + (secondsLeft * 1000));
187 | },
188 | updateCountdown: function updateCountdown(ts) {
189 | if(this.timer.length > 2) {
190 | this.timer.splice(2);
191 | }
192 |
193 | var newTime = {
194 | id: guid(),
195 | value: ((padDigits(ts.minutes, 2)) + ":" + (padDigits(ts.seconds, 2)))
196 | };
197 |
198 | this.timer.unshift(newTime);
199 | },
200 | pauseTimer: function pauseTimer() {
201 | window.clearInterval(this.countdown);
202 | },
203 | continueTimer: function continueTimer() {
204 | if(this.secondsLeft > 0) {
205 | this.startTimer(this.secondsLeft-1);
206 | }
207 | },
208 | giveWarning: function giveWarning() {
209 | speechSynth.text = this.activeReminder.warningMsg;
210 | window.speechSynthesis.speak(speechSynth);
211 | },
212 | timeIsUpMessage: function timeIsUpMessage() {
213 | speechSynth.text = this.activeReminder.timeIsUpMsg;
214 | window.speechSynthesis.speak(speechSynth);
215 | },
216 | timerResetMessage: function timerResetMessage() {
217 | speechSynth.text = "Timer reset. Time left " + (this.activeReminder.durationInMinutes) + " " + (this.activeReminder.durationInMinutes > 1 ? 'minutes': 'minute');
218 | window.speechSynthesis.speak(speechSynth);
219 | },
220 | reset: function reset() {
221 | this.resetTimer();
222 | this.timerResetMessage();
223 | },
224 | startListenVoiceCommands: function startListenVoiceCommands() {
225 | var this$1 = this;
226 |
227 | if(this.isListening || !this.supportSpeechRecognition) { return; }
228 |
229 | this.isListening = true;
230 | recognition.start();
231 | recognition.onresult = function (event) {
232 | var last = event.results.length - 1;
233 | var transcript = event.results[last][0].transcript;
234 | var splittedTranscript = transcript.split(' ');
235 | var isFinal = event.results[last].isFinal;
236 |
237 | this$1.tooltipText = transcript;
238 |
239 | if(transcript == "reset") {
240 | this$1.resetTimer();
241 | this$1.timerResetMessage();
242 | }
243 | if(
244 | splittedTranscript.length >= 3 &&
245 | splittedTranscript[0] == 'timer' &&
246 | isFinal &&
247 | numbers.includes(Number(splittedTranscript[1])) &&
248 | (splittedTranscript[2] == 'minute' || splittedTranscript[2] == 'minutes')
249 | ) {
250 | this$1.activeReminder.durationInMinutes = numbers[splittedTranscript[1]];
251 | this$1.resetTimer();
252 | this$1.timerResetMessage();
253 | }
254 |
255 |
256 | }
257 | recognition.onend = function () {
258 | this$1.isListening = false;
259 | this$1.tooltipText = '';
260 | recognition.stop();
261 | }
262 | recognition.onsoundend = function () {
263 | this$1.isListening = false;
264 | recognition.stop();
265 | }
266 | },
267 | mouseOver: function mouseOver(type) {
268 | this.stageBg = settings[type].stageBg;
269 | },
270 | mouseOut: function mouseOut() {
271 | this.stageBg = this.activeReminder.stageBg;
272 | }
273 | }
274 | });
275 |
276 |
--------------------------------------------------------------------------------
/docs/style.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css?family=Roboto:100,400");
2 | *, *::after, *::before {
3 | box-sizing: border-box; }
4 |
5 | html, body {
6 | height: 100%;
7 | min-height: 100%; }
8 |
9 | body {
10 | margin: 0;
11 | display: flex;
12 | align-items: center;
13 | justify-content: center;
14 | font-family: 'Roboto', sans-serif;
15 | background: linear-gradient(to bottom right, #c8c897, #6590A2); }
16 |
17 | [v-cloak] {
18 | display: none; }
19 |
20 | a {
21 | -webkit-tap-highlight-color: transparent;
22 | user-select: none; }
23 |
24 | ::-webkit-scrollbar {
25 | display: none; }
26 |
27 | .stage {
28 | position: relative;
29 | overflow: hidden;
30 | width: 100%;
31 | height: 100%;
32 | background: #fff;
33 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
34 | background: #1E384C;
35 | transition: background-color .3s; }
36 | @media (min-width: 500px) {
37 | .stage {
38 | border-radius: 5px;
39 | max-height: 550px;
40 | max-width: 350px; } }
41 | .stage.menu-open .microphone {
42 | transform: translate3d(-1em, 0, 0);
43 | opacity: 0; }
44 | .stage.menu-open .voices-menu__button {
45 | z-index: 40;
46 | transform: translate3d(0, 0, 0);
47 | opacity: 1; }
48 | .stage.menu-open .menu {
49 | z-index: 25; }
50 | .stage.menu-open .time {
51 | transform: translate3d(0, -200%, 0);
52 | transition: .5s opacity, .5s transform;
53 | opacity: 0; }
54 | .stage.menu-open button {
55 | transform: translate3d(0, 200%, 0);
56 | transition-delay: 0s;
57 | opacity: 0; }
58 | .stage.menu-open .percent {
59 | transition: .4s opacity, .4s transform;
60 | transform: translate3d(0, 50%, 0);
61 | opacity: 0; }
62 | .stage.menu-open .menu__item {
63 | opacity: 1;
64 | transform: translate3d(0, 0, 0); }
65 | .stage.menu-open .menu__item:nth-child(1) {
66 | transition-delay: .2s; }
67 | .stage.menu-open .menu__item:nth-child(2) {
68 | transition-delay: .3s; }
69 | .stage.menu-open .menu__item:nth-child(3) {
70 | transition-delay: .4s; }
71 | .stage.voices-open .voices-menu {
72 | z-index: 35; }
73 | .stage.voices-open .voices-menu__bg {
74 | transform: scale(6); }
75 | .stage.voices-open .voices-menu__close {
76 | opacity: 1;
77 | transform: translate3d(0, 0, 0) rotate(0); }
78 | .stage.voices-open .voices-list-wrapper {
79 | opacity: 1; }
80 | .stage.voices-open .voices-list__item {
81 | opacity: 1;
82 | transform: translate(0, 0);
83 | transition: opacity .15s, transform .2s; }
84 | .stage.voices-open .voices-list__item:nth-child(1) {
85 | transition-delay: 75ms; }
86 | .stage.voices-open .voices-list__item:nth-child(2) {
87 | transition-delay: 150ms; }
88 | .stage.voices-open .voices-list__item:nth-child(3) {
89 | transition-delay: 225ms; }
90 | .stage.voices-open .voices-list__item:nth-child(4) {
91 | transition-delay: 300ms; }
92 | .stage.voices-open .voices-list__item:nth-child(5) {
93 | transition-delay: 375ms; }
94 | .stage.voices-open .voices-list__item:nth-child(6) {
95 | transition-delay: 450ms; }
96 | .stage.voices-open .voices-list__item:nth-child(7) {
97 | transition-delay: 525ms; }
98 | .stage.voices-open .voices-list__item:nth-child(8) {
99 | transition-delay: 600ms; }
100 | .stage.voices-open .voices-list__item:nth-child(9) {
101 | transition-delay: 675ms; }
102 | .stage.voices-open .voices-list__item:nth-child(10) {
103 | transition-delay: 750ms; }
104 |
105 | .microphone {
106 | z-index: 30;
107 | position: absolute;
108 | top: -.5em;
109 | left: -.8em;
110 | width: 70px;
111 | height: 70px;
112 | display: flex;
113 | justify-content: center;
114 | align-items: center;
115 | cursor: pointer;
116 | color: rgba(255, 255, 255, 0.5);
117 | transition: opacity .3s, transform .3s, color .2s; }
118 | .microphone:hover {
119 | color: rgba(255, 255, 255, 0.8); }
120 | .microphone svg {
121 | z-index: 2;
122 | position: relative;
123 | font-size: 2em;
124 | width: 1em;
125 | height: 1em; }
126 | .microphone:before, .microphone:after {
127 | content: '';
128 | position: absolute;
129 | top: 0;
130 | left: 0;
131 | width: 100%;
132 | height: 100%;
133 | border-radius: 50%;
134 | opacity: 0; }
135 | .microphone:after {
136 | z-index: 1;
137 | background: rgba(255, 255, 255, 0.1);
138 | transition: opacity .3s; }
139 | .microphone:before {
140 | z-index: 2;
141 | border: 3px solid rgba(255, 255, 255, 0.1);
142 | opacity: 0; }
143 | .microphone.is-listening {
144 | color: #d82e2e; }
145 | .microphone.is-listening:before {
146 | animation: pulseAway 1s infinite; }
147 | .microphone.is-listening:after {
148 | opacity: 1;
149 | animation: pulse 1.5s linear infinite; }
150 | .microphone .voice-tooltip {
151 | position: absolute;
152 | top: 110%;
153 | left: 25px;
154 | padding: .4em .6em;
155 | color: rgba(255, 255, 255, 0.8);
156 | font-size: .8em;
157 | font-weight: 300;
158 | text-transform: uppercase;
159 | white-space: nowrap;
160 | background: rgba(255, 255, 255, 0.1);
161 | border-radius: 3px; }
162 | .microphone .voice-tooltip:before {
163 | content: '';
164 | position: absolute;
165 | bottom: 100%;
166 | left: 5px;
167 | width: 0;
168 | height: 0;
169 | border-style: solid;
170 | border-width: 0 5px 5px 5px;
171 | border-color: transparent transparent rgba(255, 255, 255, 0.1) transparent; }
172 |
173 | .fade-enter-active,
174 | .fade-leave-active {
175 | transition: all .15s ease; }
176 |
177 | .fade-enter, .fade-leave-to {
178 | opacity: 0; }
179 |
180 | .voices-menu {
181 | position: absolute;
182 | top: 0;
183 | left: 0;
184 | right: 0;
185 | bottom: 0; }
186 | @media (min-width: 500px) {
187 | .voices-menu {
188 | border-radius: 5px;
189 | overflow: hidden; } }
190 | .voices-menu__bg {
191 | position: absolute;
192 | top: -15em;
193 | left: -15em;
194 | transform-origin: 50% 50%;
195 | width: 20em;
196 | height: 20em;
197 | color: #222;
198 | transform: scale(0.2);
199 | transition: transform .3s; }
200 | .voices-menu__button {
201 | position: absolute;
202 | top: 0;
203 | left: 0;
204 | padding: .8em .6em;
205 | color: rgba(255, 255, 255, 0.5);
206 | opacity: 0;
207 | cursor: pointer;
208 | transform: translate3d(1em, 0, 0);
209 | transition: opacity .3s, transform .3s, color .2s; }
210 | .voices-menu__button:hover {
211 | color: rgba(255, 255, 255, 0.8); }
212 | .voices-menu__button > * {
213 | vertical-align: middle;
214 | font-weight: 300;
215 | letter-spacing: 1px; }
216 | .voices-menu__button svg {
217 | width: 1.8em;
218 | height: 1.8em; }
219 | .voices-menu__close {
220 | position: absolute;
221 | top: 0;
222 | right: 0;
223 | padding: 10px 10px;
224 | font-size: 2em;
225 | font-weight: 300;
226 | color: rgba(255, 255, 255, 0.5);
227 | opacity: 0;
228 | cursor: pointer;
229 | transform: translate3d(1em, 0, 0) rotate(45deg);
230 | transition: opacity .3s, transform .3s, color .2s; }
231 | .voices-menu__close svg {
232 | width: 1em;
233 | height: 1em; }
234 | .voices-menu__close:hover {
235 | color: rgba(255, 255, 255, 0.8); }
236 |
237 | .voices-list-wrapper {
238 | position: absolute;
239 | top: 60px;
240 | left: 0;
241 | bottom: 0;
242 | right: 0;
243 | overflow-y: auto;
244 | opacity: 0; }
245 |
246 | .voices-list {
247 | margin: 0;
248 | padding: 0; }
249 | .voices-list__item {
250 | display: block;
251 | opacity: 0;
252 | transform: translate(0, 1em); }
253 | .voices-list__item.is-selected .voices-list__icon {
254 | opacity: 1;
255 | transform: translate3d(0, 0, 0) rotate(0); }
256 | .voices-list__icon {
257 | position: relative;
258 | margin-right: 20px;
259 | color: #02C39A;
260 | opacity: 0;
261 | transform: translate3d(-1em, 0, 0) rotate(-30deg);
262 | transition: opacity .2s, transform .2s; }
263 | .voices-list__icon svg {
264 | width: 1.2em;
265 | height: 1.2em; }
266 | .voices-list__link {
267 | display: flex;
268 | padding: .5em 1.1em;
269 | font-size: 1.3em;
270 | font-weight: 300;
271 | color: rgba(255, 255, 255, 0.8);
272 | text-decoration: none; }
273 | .voices-list__link:hover {
274 | background: rgba(255, 255, 255, 0.05); }
275 | .voices-list__link span {
276 | display: inline-block;
277 | vertical-align: middle; }
278 | .voices-list__content {
279 | line-height: 1; }
280 | .voices-list__content span {
281 | font-size: .5em; }
282 | .voices-list__default {
283 | color: #02C39A; }
284 |
285 | .menu {
286 | z-index: 10;
287 | position: absolute;
288 | width: 100%;
289 | height: 100%;
290 | display: flex;
291 | justify-content: center;
292 | align-items: center; }
293 | .menu__button {
294 | z-index: 30;
295 | position: absolute;
296 | top: 0;
297 | right: 0;
298 | display: inline-block;
299 | padding: 1.5em 1em;
300 | cursor: pointer; }
301 | .menu__button:hover .menu__dot,
302 | .menu__button:hover .menu__dot:before,
303 | .menu__button:hover .menu__dot:after {
304 | background: rgba(255, 255, 255, 0.8); }
305 | .menu__dot {
306 | position: relative;
307 | border-radius: 50%;
308 | width: 6px;
309 | height: 6px;
310 | background: rgba(255, 255, 255, 0.5);
311 | transition: background .2s; }
312 | .menu__dot:before, .menu__dot:after {
313 | position: absolute;
314 | content: '';
315 | border-radius: 50%;
316 | width: 6px;
317 | height: 6px;
318 | background: rgba(255, 255, 255, 0.5);
319 | transition: background .2s; }
320 | .menu__dot:before {
321 | top: 10px; }
322 | .menu__dot:after {
323 | bottom: 10px; }
324 | .menu__list {
325 | list-style: none;
326 | padding: 0;
327 | margin: 0;
328 | width: 100%; }
329 | .menu__item {
330 | overflow: hidden;
331 | opacity: 0;
332 | transform: translate3d(0, 100%, 0);
333 | transition: .4s transform, .4s opacity; }
334 | .menu__item a {
335 | font-size: 1.8em;
336 | font-weight: 300;
337 | display: block;
338 | color: rgba(255, 255, 255, 0.5);
339 | text-transform: uppercase;
340 | text-decoration: none;
341 | padding: .5em 1.5em; }
342 | .menu__item a span {
343 | display: inline-block;
344 | vertical-align: middle;
345 | transition: transform .3s; }
346 | .menu__item a:hover svg, .menu__item a:focus svg {
347 | transform: scale(1.2); }
348 | .menu__item a:hover .water-glass__water, .menu__item a:focus .water-glass__water {
349 | fill: #32BAFA;
350 | transform: scale(1, 0.8); }
351 | .menu__item a:hover .coffee-cup__coffee, .menu__item a:focus .coffee-cup__coffee {
352 | fill: #BF9E87;
353 | transform: scale(1, 0.8); }
354 | .menu__item a:hover .clock__short, .menu__item a:focus .clock__short {
355 | fill: #02C39A;
356 | transform-origin: 0% 50%;
357 | transform: rotate(20deg);
358 | transition: transform 1s, color .2s; }
359 | .menu__item a:hover .clock__long, .menu__item a:focus .clock__long {
360 | fill: #02C39A;
361 | transition: transform 1s, color .2s;
362 | transform-origin: 50% 95%;
363 | transform: rotate(360deg); }
364 | .menu__item svg {
365 | display: inline-block;
366 | vertical-align: middle;
367 | width: 1em;
368 | height: 1em;
369 | margin-right: 1em;
370 | transition: transform .3s; }
371 | .menu__item svg path {
372 | fill: #fff;
373 | transition: all .3s;
374 | transform-origin: 100% 100%; }
375 |
376 | .browser-support {
377 | color: #fff;
378 | font-size: .8rem;
379 | text-align: center;
380 | padding: .5rem; }
381 |
382 | .content {
383 | z-index: 20;
384 | position: absolute;
385 | bottom: 0;
386 | left: 0;
387 | width: 100%;
388 | height: 100%;
389 | display: flex;
390 | align-items: center;
391 | justify-content: center; }
392 |
393 | .time {
394 | overflow: hidden;
395 | padding: 1em;
396 | font-size: 1.1em;
397 | text-align: center;
398 | transition: .5s .2s opacity, .5s transform .2s; }
399 |
400 | .timer__item {
401 | transition: all 1s;
402 | margin-right: 10px;
403 | color: rgba(255, 255, 255, 0.8); }
404 | .timer__item:first-child, .timer__item:nth-child(3) {
405 | color: rgba(255, 255, 255, 0.2); }
406 |
407 | .timer-enter, .timer-leave-to {
408 | opacity: 0;
409 | transform: translate3d(0, -100%, 0); }
410 |
411 | .timer-leave-to {
412 | transition-duration: .5s; }
413 |
414 | .timer-leave-active {
415 | transform: translate3d(0, 0, 0); }
416 |
417 | .percent {
418 | z-index: 2;
419 | position: relative;
420 | font-size: 7em;
421 | font-weight: 100;
422 | color: rgba(255, 255, 255, 0.7);
423 | transition: .4s .2s opacity, .4s .2s transform; }
424 | .percent > div {
425 | display: inline-block; }
426 | .percent > span {
427 | margin-left: -.4em;
428 | font-size: .5em; }
429 |
430 | .percent-left-enter-active, .percent-left-leave-active {
431 | transition: transform .1s ease; }
432 |
433 | .percent-left-enter, .percent-left-leave-to {
434 | transform: scale(1.05); }
435 |
436 | button {
437 | z-index: 20;
438 | position: absolute;
439 | display: block;
440 | width: 70%;
441 | margin: auto;
442 | left: 0;
443 | right: 0;
444 | bottom: 1.5em;
445 | padding: .6em;
446 | color: rgba(255, 255, 255, 0.8);
447 | font-size: 1.1em;
448 | font-weight: 300;
449 | letter-spacing: 1px;
450 | text-transform: uppercase;
451 | background: transparent;
452 | border: 1px solid rgba(255, 255, 255, 0.8);
453 | border-radius: 2em;
454 | outline: none;
455 | transition: .2s background, .4s .3s transform, .4s .3s opacity;
456 | cursor: pointer; }
457 | button:hover {
458 | background: #fff;
459 | color: currentColor; }
460 |
461 | .waves {
462 | position: absolute;
463 | bottom: 0;
464 | left: 0;
465 | width: 100%;
466 | height: 100%;
467 | overflow: hidden;
468 | transition: .4s transform ease;
469 | transform-origin: bottom center; }
470 | @media (min-width: 500px) {
471 | .waves {
472 | border-radius: 5px; } }
473 |
474 | .wave {
475 | position: absolute;
476 | bottom: 0;
477 | left: 0;
478 | width: 100%;
479 | height: 100%;
480 | animation: wave 1s linear infinite; }
481 | .wave--front {
482 | z-index: 2;
483 | color: #32BAFA; }
484 | .wave--back {
485 | z-index: 1;
486 | color: #2C7FBE;
487 | animation-direction: reverse; }
488 |
489 | .water {
490 | position: absolute;
491 | bottom: 0;
492 | left: 0;
493 | width: 100%;
494 | height: 80%;
495 | background: currentColor; }
496 | .water svg {
497 | position: absolute;
498 | width: 100%;
499 | left: 0;
500 | right: 0;
501 | bottom: 99.9%; }
502 |
503 | .water:first-of-type {
504 | transform: translate(-100%, 0); }
505 |
506 | svg {
507 | fill: currentColor; }
508 |
509 | @keyframes wave {
510 | 0% {
511 | transform: translate3d(0, 0, 0); }
512 | 50% {
513 | transform: translate3d(50%, 0.5em, 0); }
514 | 100% {
515 | transform: translate3d(100%, 0, 0); } }
516 |
517 | @keyframes pulse {
518 | 0% {
519 | transform: scale(1); }
520 | 50% {
521 | transform: scale(1.1); }
522 | 100% {
523 | transform: scale(1); } }
524 |
525 | @keyframes pulseAway {
526 | 0% {
527 | opacity: 0;
528 | transform: scale(0.5); }
529 | 50% {
530 | opacity: 1; }
531 | 100% {
532 | transform: scale(1.4); } }
533 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Reminder App
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
21 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {{ tooltipText }}
33 |
34 |
35 |
36 |
75 |
125 |
126 | Your browser doesn't support speech synthesis.
127 |
128 |
129 |
130 |
131 | {{ time.value }}
132 |
133 |
134 |
135 |
153 |
154 |
155 |
156 | {{ percentsLeft }}
157 |
158 |
%
159 |
160 |
161 |
164 | {{ percentsLeft > 0 ? activeReminder.buttonTxt : 'Reset' }}
165 |
166 |
167 |
168 |
169 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reminderapp",
3 | "version": "0.0.1",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "node-sass --output-style compressed style.scss style.css",
8 | "develop": "npm run browsersync & node-sass -w style.scss style.css",
9 | "browsersync": "browser-sync start --server -f 'style.scss,script.js'"
10 | },
11 | "author": "Irko Palenius",
12 | "license": "MIT",
13 | "devDependencies": {
14 | "browser-sync": "latest",
15 | "node-sass": "latest"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/script.es5.js:
--------------------------------------------------------------------------------
1 | var SpeechRecognition = SpeechRecognition || window.webkitSpeechRecognition || undefined;
2 | var numbers = Array.apply(null, Array(101)).map(function (_, i) {return i;});
3 |
4 | if(SpeechRecognition) {
5 | var SpeechGrammarList = SpeechGrammarList || window.webkitSpeechGrammarList || undefined;
6 | var SpeechRecognitionEvent = SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent || undefined;
7 |
8 | var commands = ['reset', 'timer' ].concat( numbers);
9 | var grammar = '#JSGF V1.0; grammar colors; public = ' + commands.join(' | ') + ' ;'
10 |
11 | var speechRecognitionList = new SpeechGrammarList();
12 | speechRecognitionList.addFromString(grammar, 1);
13 |
14 | var recognition = new SpeechRecognition();
15 | recognition.grammars = speechRecognitionList;
16 | //recognition.continuous = false;
17 | recognition.lang = 'en-US';
18 | recognition.interimResults = true;
19 | recognition.maxAlternatives = 1;
20 | }
21 |
22 | var speechSynth = new SpeechSynthesisUtterance();
23 |
24 | var padDigits = function (number, digits) {
25 | return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number;
26 | }
27 |
28 | var calculatePercentsLeft = function (value, from) {
29 | return Math.floor(Math.ceil(value/1000) / (from * 60) * 100)
30 | }
31 |
32 | var calculateScaleFactor = function (percent) {
33 | return 1-(100-percent)/100;
34 | }
35 |
36 | function guid() {
37 | function s4() {
38 | return Math.floor((1 + Math.random()) * 0x10000)
39 | .toString(16)
40 | .substring(1);
41 | }
42 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
43 | s4() + '-' + s4() + s4() + s4();
44 | }
45 |
46 | var settings = {
47 | water: {
48 | warningMsg: 'Remember to drink',
49 | timeIsUpMsg: 'Time\'s up. You really need to drink now',
50 | buttonTxt: 'Drink',
51 | waveFrontColor: '#32BAFA',
52 | waveBackColor: '#2C7FBE',
53 | stageBg: '#1E384C',
54 | durationInMinutes: 1
55 | },
56 | coffee: {
57 | warningMsg: 'It\'s almost coffee time.',
58 | timeIsUpMsg: 'Time\'s up. Let\'s take a coffee break!',
59 | buttonTxt: 'Drink coffee',
60 | waveFrontColor: '#b39374',
61 | waveBackColor: '#7a6057',
62 | stageBg: '#392a2c',
63 | durationInMinutes: 2
64 | },
65 | break: {
66 | warningMsg: 'It is time to rest your eyes soon!',
67 | timeIsUpMsg: 'Time\'s up. Now, it\'s really time to rest your eyes!',
68 | buttonTxt: 'Take a break',
69 | waveFrontColor: '#02C39A',
70 | waveBackColor: '#028090',
71 | stageBg: '#012F35',
72 | durationInMinutes: 1
73 | }
74 | };
75 |
76 | new Vue({
77 | el: '#stage',
78 | data: function data() {
79 | return {
80 | color: '',
81 | percents: [100],
82 | percentsLeft: 100,
83 | secondsLeft: 0,
84 | waveStyles: '',
85 | duration: 1,
86 | timer: [],
87 | voicesOpen: false,
88 | voices: [],
89 | selectedVoice: {},
90 | countdownObj: {},
91 | activeReminder: settings.water,
92 | menuOpen: false,
93 | isListening: false,
94 | tooltipText: 'Say eg. "reset"',
95 | stageBg: settings.water.stageBg
96 | }
97 | },
98 | mounted: function mounted() {
99 | var this$1 = this;
100 |
101 | this.resetTimer();
102 | this.voices = speechSynthesis.getVoices();
103 |
104 | if(this.voices.length === 0) {
105 | speechSynthesis.onvoiceschanged = function () {
106 | this$1.voices = speechSynthesis.getVoices();
107 | };
108 | }
109 | },
110 | computed: {
111 | supportSpeechSynth: function supportSpeechSynth() {
112 | return 'speechSynthesis' in window;
113 | },
114 | supportSpeechRecognition: function supportSpeechRecognition() {
115 | return SpeechRecognition;
116 | }
117 | },
118 | watch: {
119 | percentsLeft: function(val, oldVal) {
120 | if (val === oldVal) {
121 | return;
122 | }
123 | this.percents.splice(0, 1);
124 | this.percents.push(val);
125 | }
126 | },
127 | methods: {
128 | setActiveReminder: function setActiveReminder(reminder) {
129 | this.activeReminder = settings[reminder];
130 | this.stageBg = this.activeReminder.stageBg;
131 | },
132 | toggleMenu: function toggleMenu() {
133 | this.menuOpen = !this.menuOpen;
134 | if(this.menuOpen) {
135 | this.pauseTimer();
136 | this.waveStyles = "transform: translate3d(0,100%,0); transition-delay: .25s;";
137 | }else {
138 | this.continueTimer();
139 | }
140 | },
141 | toggleVoicesMenu: function toggleVoicesMenu() {
142 | this.voicesOpen = !this.voicesOpen;
143 | },
144 | voiceSelected: function voiceSelected(voice) {
145 | this.selectedVoice = voice;
146 | speechSynth.voice = voice;
147 | },
148 | start: function start(reminder) {
149 | this.setActiveReminder(reminder);
150 | this.percents = [100];
151 | this.timer = [];
152 | this.menuOpen = false;
153 | this.resetTimer();
154 | },
155 | resetTimer: function resetTimer() {
156 | var durationInSeconds = 60 * this.activeReminder.durationInMinutes;
157 | this.startTimer(durationInSeconds);
158 | },
159 | startTimer: function startTimer(secondsLeft) {
160 | var this$1 = this;
161 |
162 | var now = new Date();
163 |
164 | // later on, this timer may be stopped
165 | if(this.countdown) {
166 | window.clearInterval(this.countdown);
167 | }
168 |
169 | this.countdown = countdown(function (ts) {
170 | this$1.secondsLeft= Math.ceil(ts.value/1000);
171 | this$1.percentsLeft = calculatePercentsLeft(ts.value,this$1.activeReminder.durationInMinutes);
172 | this$1.waveStyles = "transform: scale(1," + (calculateScaleFactor(this$1.percentsLeft)) + ")";
173 | this$1.updateCountdown(ts);
174 | if(this$1.percentsLeft == 10) {
175 | this$1.giveWarning();
176 | }
177 | if(this$1.percentsLeft <= 0){
178 | this$1.timeIsUpMessage();
179 | this$1.pauseTimer();
180 | this$1.timer = [];
181 | setTimeout(function () {
182 | this$1.startListenVoiceCommands();
183 | }, 1500);
184 |
185 | }
186 | }, now.getTime() + (secondsLeft * 1000));
187 | },
188 | updateCountdown: function updateCountdown(ts) {
189 | if(this.timer.length > 2) {
190 | this.timer.splice(2);
191 | }
192 |
193 | var newTime = {
194 | id: guid(),
195 | value: ((padDigits(ts.minutes, 2)) + ":" + (padDigits(ts.seconds, 2)))
196 | };
197 |
198 | this.timer.unshift(newTime);
199 | },
200 | pauseTimer: function pauseTimer() {
201 | window.clearInterval(this.countdown);
202 | },
203 | continueTimer: function continueTimer() {
204 | if(this.secondsLeft > 0) {
205 | this.startTimer(this.secondsLeft-1);
206 | }
207 | },
208 | giveWarning: function giveWarning() {
209 | speechSynth.text = this.activeReminder.warningMsg;
210 | window.speechSynthesis.speak(speechSynth);
211 | },
212 | timeIsUpMessage: function timeIsUpMessage() {
213 | speechSynth.text = this.activeReminder.timeIsUpMsg;
214 | window.speechSynthesis.speak(speechSynth);
215 | },
216 | timerResetMessage: function timerResetMessage() {
217 | speechSynth.text = "Timer reset. Time left " + (this.activeReminder.durationInMinutes) + " " + (this.activeReminder.durationInMinutes > 1 ? 'minutes': 'minute');
218 | window.speechSynthesis.speak(speechSynth);
219 | },
220 | reset: function reset() {
221 | this.resetTimer();
222 | this.timerResetMessage();
223 | },
224 | startListenVoiceCommands: function startListenVoiceCommands() {
225 | var this$1 = this;
226 |
227 | if(this.isListening || !this.supportSpeechRecognition) { return; }
228 |
229 | this.isListening = true;
230 | recognition.start();
231 | recognition.onresult = function (event) {
232 | var last = event.results.length - 1;
233 | var transcript = event.results[last][0].transcript;
234 | var splittedTranscript = transcript.split(' ');
235 | var isFinal = event.results[last].isFinal;
236 |
237 | this$1.tooltipText = transcript;
238 |
239 | if(transcript == "reset") {
240 | this$1.resetTimer();
241 | this$1.timerResetMessage();
242 | }
243 | if(
244 | splittedTranscript.length >= 3 &&
245 | splittedTranscript[0] == 'timer' &&
246 | isFinal &&
247 | numbers.includes(Number(splittedTranscript[1])) &&
248 | (splittedTranscript[2] == 'minute' || splittedTranscript[2] == 'minutes')
249 | ) {
250 | this$1.activeReminder.durationInMinutes = numbers[splittedTranscript[1]];
251 | this$1.resetTimer();
252 | this$1.timerResetMessage();
253 | }
254 |
255 |
256 | }
257 | recognition.onend = function () {
258 | this$1.isListening = false;
259 | this$1.tooltipText = '';
260 | recognition.stop();
261 | }
262 | recognition.onsoundend = function () {
263 | this$1.isListening = false;
264 | recognition.stop();
265 | }
266 | },
267 | mouseOver: function mouseOver(type) {
268 | this.stageBg = settings[type].stageBg;
269 | },
270 | mouseOut: function mouseOut() {
271 | this.stageBg = this.activeReminder.stageBg;
272 | }
273 | }
274 | });
275 |
276 |
--------------------------------------------------------------------------------
/script.js:
--------------------------------------------------------------------------------
1 | var SpeechRecognition = SpeechRecognition || window.webkitSpeechRecognition || undefined;
2 | var numbers = Array.apply(null, Array(101)).map(function (_, i) {return i;});
3 |
4 | if(SpeechRecognition) {
5 | var SpeechGrammarList = SpeechGrammarList || window.webkitSpeechGrammarList || undefined;
6 | var SpeechRecognitionEvent = SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent || undefined;
7 |
8 | var commands = ['reset', 'timer', ...numbers];
9 | var grammar = '#JSGF V1.0; grammar colors; public = ' + commands.join(' | ') + ' ;'
10 |
11 | var speechRecognitionList = new SpeechGrammarList();
12 | speechRecognitionList.addFromString(grammar, 1);
13 |
14 | var recognition = new SpeechRecognition();
15 | recognition.grammars = speechRecognitionList;
16 | //recognition.continuous = false;
17 | recognition.lang = 'en-US';
18 | recognition.interimResults = true;
19 | recognition.maxAlternatives = 1;
20 | }
21 |
22 | var speechSynth = new SpeechSynthesisUtterance();
23 |
24 | const padDigits = (number, digits) => {
25 | return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number;
26 | }
27 |
28 | const calculatePercentsLeft = (value, from) => {
29 | return Math.floor(Math.ceil(value/1000) / (from * 60) * 100)
30 | }
31 |
32 | const calculateScaleFactor = (percent) => {
33 | return 1-(100-percent)/100;
34 | }
35 |
36 | function guid() {
37 | function s4() {
38 | return Math.floor((1 + Math.random()) * 0x10000)
39 | .toString(16)
40 | .substring(1);
41 | }
42 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
43 | s4() + '-' + s4() + s4() + s4();
44 | }
45 |
46 | const settings = {
47 | water: {
48 | warningMsg: 'Remember to drink',
49 | timeIsUpMsg: 'Time\'s up. You really need to drink now',
50 | buttonTxt: 'Drink',
51 | waveFrontColor: '#32BAFA',
52 | waveBackColor: '#2C7FBE',
53 | stageBg: '#1E384C',
54 | durationInMinutes: 1
55 | },
56 | coffee: {
57 | warningMsg: 'It\'s almost coffee time.',
58 | timeIsUpMsg: 'Time\'s up. Let\'s take a coffee break!',
59 | buttonTxt: 'Drink coffee',
60 | waveFrontColor: '#b39374',
61 | waveBackColor: '#7a6057',
62 | stageBg: '#392a2c',
63 | durationInMinutes: 2
64 | },
65 | break: {
66 | warningMsg: 'It is time to rest your eyes soon!',
67 | timeIsUpMsg: 'Time\'s up. Now, it\'s really time to rest your eyes!',
68 | buttonTxt: 'Take a break',
69 | waveFrontColor: '#02C39A',
70 | waveBackColor: '#028090',
71 | stageBg: '#012F35',
72 | durationInMinutes: 1
73 | }
74 | };
75 |
76 | new Vue({
77 | el: '#stage',
78 | data() {
79 | return {
80 | color: '',
81 | percents: [100],
82 | percentsLeft: 100,
83 | secondsLeft: 0,
84 | waveStyles: '',
85 | duration: 1,
86 | timer: [],
87 | voicesOpen: false,
88 | voices: [],
89 | selectedVoice: {},
90 | countdownObj: {},
91 | activeReminder: settings.water,
92 | menuOpen: false,
93 | isListening: false,
94 | tooltipText: 'Say eg. "reset"',
95 | stageBg: settings.water.stageBg
96 | }
97 | },
98 | mounted() {
99 | this.resetTimer();
100 | this.voices = speechSynthesis.getVoices();
101 |
102 | if(this.voices.length === 0) {
103 | speechSynthesis.onvoiceschanged = () => {
104 | this.voices = speechSynthesis.getVoices();
105 | };
106 | }
107 | },
108 | computed: {
109 | supportSpeechSynth() {
110 | return 'speechSynthesis' in window;
111 | },
112 | supportSpeechRecognition() {
113 | return SpeechRecognition;
114 | }
115 | },
116 | watch: {
117 | percentsLeft: function(val, oldVal) {
118 | if (val === oldVal) {
119 | return;
120 | }
121 | this.percents.splice(0, 1);
122 | this.percents.push(val);
123 | }
124 | },
125 | methods: {
126 | setActiveReminder(reminder) {
127 | this.activeReminder = settings[reminder];
128 | this.stageBg = this.activeReminder.stageBg;
129 | },
130 | toggleMenu() {
131 | this.menuOpen = !this.menuOpen;
132 | if(this.menuOpen) {
133 | this.pauseTimer();
134 | this.waveStyles = `transform: translate3d(0,100%,0); transition-delay: .25s;`;
135 | }else {
136 | this.continueTimer();
137 | }
138 | },
139 | toggleVoicesMenu() {
140 | this.voicesOpen = !this.voicesOpen;
141 | },
142 | voiceSelected(voice) {
143 | this.selectedVoice = voice;
144 | speechSynth.voice = voice;
145 | },
146 | start(reminder) {
147 | this.setActiveReminder(reminder);
148 | this.percents = [100];
149 | this.timer = [];
150 | this.menuOpen = false;
151 | this.resetTimer();
152 | },
153 | resetTimer() {
154 | let durationInSeconds = 60 * this.activeReminder.durationInMinutes;
155 | this.startTimer(durationInSeconds);
156 | },
157 | startTimer(secondsLeft) {
158 | let now = new Date();
159 |
160 | // later on, this timer may be stopped
161 | if(this.countdown) {
162 | window.clearInterval(this.countdown);
163 | }
164 |
165 | this.countdown = countdown(ts => {
166 | this.secondsLeft= Math.ceil(ts.value/1000);
167 | this.percentsLeft = calculatePercentsLeft(ts.value,this.activeReminder.durationInMinutes);
168 | this.waveStyles = `transform: scale(1,${calculateScaleFactor(this.percentsLeft)})`;
169 | this.updateCountdown(ts);
170 | if(this.percentsLeft == 10) {
171 | this.giveWarning();
172 | }
173 | if(this.percentsLeft <= 0){
174 | this.timeIsUpMessage();
175 | this.pauseTimer();
176 | this.timer = [];
177 | setTimeout(() => {
178 | this.startListenVoiceCommands();
179 | }, 1500);
180 |
181 | }
182 | }, now.getTime() + (secondsLeft * 1000));
183 | },
184 | updateCountdown(ts) {
185 | if(this.timer.length > 2) {
186 | this.timer.splice(2);
187 | }
188 |
189 | const newTime = {
190 | id: guid(),
191 | value: `${padDigits(ts.minutes, 2)}:${padDigits(ts.seconds, 2)}`
192 | };
193 |
194 | this.timer.unshift(newTime);
195 | },
196 | pauseTimer() {
197 | window.clearInterval(this.countdown);
198 | },
199 | continueTimer() {
200 | if(this.secondsLeft > 0) {
201 | this.startTimer(this.secondsLeft-1);
202 | }
203 | },
204 | giveWarning() {
205 | speechSynth.text = this.activeReminder.warningMsg;
206 | window.speechSynthesis.speak(speechSynth);
207 | },
208 | timeIsUpMessage() {
209 | speechSynth.text = this.activeReminder.timeIsUpMsg;
210 | window.speechSynthesis.speak(speechSynth);
211 | },
212 | timerResetMessage() {
213 | speechSynth.text = `Timer reset. Time left ${this.activeReminder.durationInMinutes} ${this.activeReminder.durationInMinutes > 1 ? 'minutes': 'minute'}`;
214 | window.speechSynthesis.speak(speechSynth);
215 | },
216 | reset() {
217 | this.resetTimer();
218 | this.timerResetMessage();
219 | },
220 | startListenVoiceCommands() {
221 | if(this.isListening || !this.supportSpeechRecognition) return;
222 |
223 | this.isListening = true;
224 | recognition.start();
225 | recognition.onresult = (event) => {
226 | let last = event.results.length - 1;
227 | let transcript = event.results[last][0].transcript;
228 | let splittedTranscript = transcript.split(' ');
229 | let isFinal = event.results[last].isFinal;
230 |
231 | this.tooltipText = transcript;
232 |
233 | if(transcript == "reset") {
234 | this.resetTimer();
235 | this.timerResetMessage();
236 | }
237 | if(
238 | splittedTranscript.length >= 3 &&
239 | splittedTranscript[0] == 'timer' &&
240 | isFinal &&
241 | numbers.includes(Number(splittedTranscript[1])) &&
242 | (splittedTranscript[2] == 'minute' || splittedTranscript[2] == 'minutes')
243 | ) {
244 | this.activeReminder.durationInMinutes = numbers[splittedTranscript[1]];
245 | this.resetTimer();
246 | this.timerResetMessage();
247 | }
248 |
249 |
250 | }
251 | recognition.onend = () => {
252 | this.isListening = false;
253 | this.tooltipText = '';
254 | recognition.stop();
255 | }
256 | recognition.onsoundend = () => {
257 | this.isListening = false;
258 | recognition.stop();
259 | }
260 | },
261 | mouseOver(type) {
262 | this.stageBg = settings[type].stageBg;
263 | },
264 | mouseOut() {
265 | this.stageBg = this.activeReminder.stageBg;
266 | }
267 | }
268 | });
269 |
--------------------------------------------------------------------------------
/style.scss:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css?family=Roboto:100,400');
2 |
3 | $blue-dark: #1E384C;
4 | $blue: #2C7FBE;
5 | $blue-light: #32BAFA;
6 | $green: #02C39A;
7 |
8 | $stage-bg: $blue-dark;
9 |
10 | *, *::after, *::before {
11 | box-sizing: border-box;
12 | }
13 | html, body {
14 | height: 100%;
15 | min-height: 100%;
16 | }
17 | body {
18 | margin: 0;
19 | display: flex;
20 | align-items: center;
21 | justify-content: center;
22 | font-family: 'Roboto', sans-serif;
23 | background: linear-gradient(to bottom right, #c8c897, #6590A2);
24 | }
25 | [v-cloak] { display:none; }
26 | a {
27 | -webkit-tap-highlight-color: rgba(0,0,0,0);
28 | user-select: none;
29 | }
30 |
31 | ::-webkit-scrollbar {
32 | display: none;
33 | }
34 |
35 | .stage {
36 | position: relative;
37 | overflow: hidden;
38 | width: 100%;
39 | height: 100%;
40 | background: #fff;
41 | box-shadow: 0 10px 20px rgba(0,0,0,0.2);
42 | background: $stage-bg;
43 | transition: background-color .3s;
44 |
45 | @media (min-width: 500px) {
46 | border-radius: 5px;
47 | max-height: 550px;
48 | max-width: 350px;
49 | }
50 |
51 | &.menu-open {
52 | .microphone {
53 | transform: translate3d(-1em,0,0);
54 | opacity: 0;
55 | }
56 | .voices-menu__button {
57 | z-index: 40;
58 | transform: translate3d(0,0,0);
59 | opacity: 1;
60 | }
61 | .menu {
62 | z-index: 25;
63 | }
64 | .time {
65 | transform: translate3d(0,-200%,0);
66 | transition: .5s opacity, .5s transform;
67 | opacity: 0;
68 | }
69 | button {
70 | transform: translate3d(0,200%,0);
71 | transition-delay: 0s;
72 | opacity: 0;
73 | }
74 | .percent {
75 | transition: .4s opacity, .4s transform;
76 | transform: translate3d(0,50%,0);
77 | opacity: 0;
78 | }
79 |
80 | .menu__item {
81 | opacity: 1;
82 | transform: translate3d(0,0,0);
83 |
84 | &:nth-child(1) {
85 | transition-delay: .2s;
86 | }
87 | &:nth-child(2) {
88 | transition-delay: .3s;
89 | }
90 | &:nth-child(3) {
91 | transition-delay: .4s;
92 | }
93 | }
94 | }
95 |
96 | &.voices-open {
97 | .voices-menu {
98 | z-index: 35;
99 | }
100 | .voices-menu__bg {
101 | transform: scale(6);
102 | }
103 |
104 | .voices-menu__close {
105 | opacity: 1;
106 | transform: translate3d(0,0,0) rotate(0);
107 | }
108 |
109 | .voices-list-wrapper {
110 | opacity: 1;
111 | }
112 | .voices-list__item {
113 | opacity: 1;
114 | transform: translate(0,0);
115 | transition: opacity .15s, transform .2s;
116 | }
117 |
118 | @for $i from 1 through 10 {
119 | .voices-list__item:nth-child(#{$i}) {
120 | transition-delay: 75ms * $i;
121 | }
122 | }
123 | }
124 | }
125 |
126 | .microphone {
127 | z-index: 30;
128 | position: absolute;
129 | top: -.5em;
130 | left: -.8em;
131 | width: 70px;
132 | height: 70px;
133 | display: flex;
134 | justify-content: center;
135 | align-items: center;
136 | cursor: pointer;
137 |
138 | color: rgba(#fff, .5);
139 | transition: opacity .3s, transform .3s, color .2s;
140 |
141 | &:hover {
142 | color: rgba(#fff, .8);
143 | }
144 |
145 | svg {
146 | z-index: 2;
147 | position: relative;
148 | font-size: 2em;
149 | width: 1em;
150 | height: 1em;
151 | }
152 | &:before,
153 | &:after {
154 | content: '';
155 | position: absolute;
156 | top: 0;
157 | left: 0;
158 | width: 100%;
159 | height: 100%;
160 | border-radius: 50%;
161 | opacity: 0;
162 | }
163 | &:after {
164 | z-index: 1;
165 | background: rgba(#fff, .1);
166 | transition: opacity .3s;
167 | }
168 |
169 | &:before {
170 | z-index: 2;
171 | border: 3px solid rgba(#fff, .1);
172 | opacity: 0;
173 | }
174 |
175 | &.is-listening {
176 | color: rgba(#D82E2E, 1);
177 | &:before {
178 | animation: pulseAway 1s infinite;
179 | }
180 | &:after {
181 | opacity: 1;
182 | animation: pulse 1.5s linear infinite;
183 | }
184 | }
185 |
186 | .voice-tooltip {
187 | position: absolute;
188 | top: 110%;
189 | left: 25px;
190 | padding: .4em .6em;
191 | color: rgba(#fff, .8);
192 |
193 | font-size: .8em;
194 | font-weight: 300;
195 | text-transform: uppercase;
196 | white-space: nowrap;
197 |
198 | background: rgba(#fff, .1);
199 | border-radius: 3px;
200 |
201 | &:before {
202 | content: '';
203 | position: absolute;
204 | bottom: 100%;
205 | left: 5px;
206 | width: 0;
207 | height: 0;
208 | border-style: solid;
209 | border-width: 0 5px 5px 5px;
210 | border-color: transparent transparent rgba(#fff, .1) transparent;
211 | }
212 | }
213 | }
214 |
215 | .fade-enter-active,
216 | .fade-leave-active {
217 | transition: all .15s ease;
218 | }
219 | .fade-enter, .fade-leave-to {
220 | opacity: 0;
221 | }
222 |
223 | .voices-menu {
224 | position: absolute;
225 | top: 0;
226 | left: 0;
227 | right: 0;
228 | bottom: 0;
229 |
230 | @media (min-width: 500px) {
231 | border-radius: 5px;
232 | overflow: hidden;
233 | }
234 |
235 | &__bg {
236 | position: absolute;
237 | top: -15em;
238 | left: -15em;
239 | transform-origin: 50% 50%;
240 | width: 20em;
241 | height: 20em;
242 | color: #222;
243 | transform: scale(0.2);
244 | transition: transform .3s;
245 | }
246 |
247 | &__button {
248 | position: absolute;
249 | top: 0;
250 | left: 0;
251 | padding: .8em .6em;
252 | color: rgba(#fff, .5);
253 | opacity: 0;
254 | cursor: pointer;
255 | transform: translate3d(1em,0,0);
256 | transition: opacity .3s, transform .3s, color .2s;
257 |
258 | &:hover {
259 | color: rgba(#fff, .8);
260 | }
261 |
262 | > * {
263 | vertical-align: middle;
264 | font-weight: 300;
265 | letter-spacing: 1px;
266 | }
267 | svg {
268 | width: 1.8em;
269 | height: 1.8em;
270 | }
271 | }
272 |
273 | &__close {
274 | position: absolute;
275 | top: 0;
276 | right: 0;
277 | padding: 10px 10px;
278 | font-size: 2em;
279 | font-weight: 300;
280 | color: rgba(#fff, .5);
281 | opacity: 0;
282 | cursor: pointer;
283 | transform: translate3d(1em,0,0) rotate(45deg);
284 | transition: opacity .3s, transform .3s, color .2s;
285 |
286 | svg {
287 | width: 1em;
288 | height: 1em;
289 | }
290 |
291 | &:hover {
292 | color: rgba(#fff, .8);
293 | }
294 | }
295 | }
296 | .voices-list-wrapper {
297 | position: absolute;
298 | top: 60px;
299 | left: 0;
300 | bottom: 0;
301 | right: 0;
302 | overflow-y: auto;
303 | opacity: 0;
304 | }
305 | .voices-list {
306 | margin: 0;
307 | padding: 0;
308 |
309 | &__item {
310 | display: block;
311 | opacity: 0;
312 | transform: translate(0,1em);
313 |
314 | &.is-selected {
315 | .voices-list__icon {
316 | opacity: 1;
317 | transform: translate3d(0,0,0) rotate(0);
318 | }
319 | }
320 | }
321 |
322 | &__icon {
323 | position: relative;
324 | margin-right: 20px;
325 | color: $green;
326 | opacity: 0;
327 | transform: translate3d(-1em,0,0) rotate(-30deg);
328 | transition: opacity .2s, transform .2s;
329 |
330 | svg {
331 | width: 1.2em;
332 | height: 1.2em;
333 | }
334 | }
335 |
336 | &__link {
337 | display: flex;
338 | padding: .5em 1.1em;
339 | font-size: 1.3em;
340 | font-weight: 300;
341 | color: rgba(#fff, .8);
342 | text-decoration: none;
343 |
344 | &:hover {
345 | background: rgba(#fff, .05);
346 | }
347 |
348 | span {
349 | display: inline-block;
350 | vertical-align: middle;
351 | }
352 | }
353 |
354 | &__content {
355 | line-height: 1;
356 | span {
357 | font-size: .5em;
358 | }
359 | }
360 |
361 | &__default {
362 | color: $green;
363 | }
364 | }
365 |
366 | .menu {
367 | z-index: 10;
368 | position: absolute;
369 | width: 100%;
370 | height: 100%;
371 | display: flex;
372 | justify-content: center;
373 | align-items: center;
374 |
375 | &__button {
376 | z-index: 30;
377 | position: absolute;
378 | top: 0;
379 | right: 0;
380 | display: inline-block;
381 | padding: 1.5em 1em;
382 | cursor: pointer;
383 |
384 | &:hover {
385 | .menu__dot,
386 | .menu__dot:before,
387 | .menu__dot:after {
388 | background: rgba(#fff, .8);
389 | }
390 | }
391 | }
392 | &__dot {
393 | position: relative;
394 | border-radius: 50%;
395 | width: 6px;
396 | height: 6px;
397 | background: rgba(#fff, .5);
398 | transition: background .2s;
399 |
400 | &:before,
401 | &:after {
402 | position: absolute;
403 | content: '';
404 | border-radius: 50%;
405 | width: 6px;
406 | height: 6px;
407 | background: rgba(#fff, .5);
408 | transition: background .2s;
409 | }
410 | &:before {
411 | top: 10px;
412 | }
413 | &:after {
414 | bottom: 10px;
415 | }
416 | }
417 |
418 | &__list {
419 | list-style: none;
420 | padding: 0;
421 | margin: 0;
422 | width: 100%;
423 | }
424 |
425 | &__item {
426 | overflow: hidden;
427 | opacity: 0;
428 | transform: translate3d(0,100%,0);
429 | transition: .4s transform, .4s opacity;
430 |
431 | a {
432 | font-size: 1.8em;
433 | font-weight: 300;
434 | display: block;
435 | color: rgba(#fff, .5);
436 | text-transform: uppercase;
437 | text-decoration: none;
438 | padding: .5em 1.5em;
439 |
440 | span {
441 | display: inline-block;
442 | vertical-align: middle;
443 | transition: transform .3s;
444 | }
445 |
446 | &:hover,
447 | &:focus {
448 | svg {
449 | transform: scale(1.2);
450 | }
451 |
452 | .water-glass__water {
453 | fill: $blue-light;
454 | transform: scale(1,.8);
455 | }
456 | .coffee-cup__coffee {
457 | fill: #BF9E87;
458 | transform: scale(1,.8);
459 | }
460 | .clock__short {
461 | fill: #02C39A;
462 | transform-origin: 0% 50%;
463 | transform: rotate(20deg);
464 | transition: transform 1s, color .2s;
465 | }
466 | .clock__long {
467 | fill: #02C39A;
468 | transition: transform 1s, color .2s;
469 | transform-origin: 50% 95%;
470 | transform: rotate(360deg);
471 | }
472 | }
473 | }
474 |
475 | svg {
476 | display: inline-block;
477 | vertical-align: middle;
478 | width: 1em;
479 | height: 1em;
480 | margin-right: 1em;
481 | transition: transform .3s;
482 |
483 | path {
484 | fill: #fff;
485 | transition: all .3s;
486 | transform-origin: 100% 100%;
487 | }
488 | }
489 |
490 | }
491 | }
492 |
493 | .browser-support {
494 | color: #fff;
495 | font-size: .8rem;
496 | text-align: center;
497 | padding: .5rem;
498 | }
499 |
500 | .content {
501 | z-index: 20;
502 | position: absolute;
503 | bottom: 0;
504 | left: 0;
505 | width: 100%;
506 | height: 100%;
507 | display: flex;
508 | align-items: center;
509 | justify-content: center;
510 | }
511 |
512 | .time {
513 | overflow: hidden;
514 | padding: 1em;
515 | font-size: 1.1em;
516 | text-align: center;
517 | transition: .5s .2s opacity, .5s transform .2s;
518 | }
519 |
520 | .timer__item {
521 | transition: all 1s;
522 | margin-right: 10px;
523 | color: rgba(#fff, .8);
524 |
525 | &:first-child,
526 | &:nth-child(3) {
527 | color: rgba(#fff,.2);
528 | }
529 | }
530 | .timer-enter, .timer-leave-to {
531 | opacity: 0;
532 | transform: translate3d(0,-100%,0);
533 | }
534 | .timer-leave-to {
535 | transition-duration: .5s;
536 | }
537 | .timer-leave-active {
538 | transform: translate3d(0,0,0);
539 | }
540 |
541 | .percent {
542 | z-index: 2;
543 | position: relative;
544 | font-size: 7em;
545 | font-weight: 100;
546 | color: rgba(#fff, 0.7);
547 | transition: .4s .2s opacity, .4s .2s transform;
548 |
549 | > div {
550 | display: inline-block;
551 | }
552 | > span {
553 | margin-left: -.4em;
554 | font-size: .5em;
555 | }
556 | }
557 | .percent-left-enter-active, .percent-left-leave-active {
558 | transition: transform .1s ease;
559 | }
560 | .percent-left-enter, .percent-left-leave-to {
561 | transform: scale(1.05);
562 | }
563 |
564 | button {
565 | z-index: 20;
566 | position: absolute;
567 | display: block;
568 | width: 70%;
569 | margin: auto;
570 | left: 0;
571 | right: 0;
572 | bottom: 1.5em;
573 | padding: .6em;
574 |
575 | color: rgba(#fff, 0.8);
576 | font-size: 1.1em;
577 | font-weight: 300;
578 | letter-spacing: 1px;
579 | text-transform: uppercase;
580 |
581 | background: transparent;
582 | border: 1px solid rgba(#fff, 0.8);
583 | border-radius: 2em;
584 | outline: none;
585 | transition: .2s background, .4s .3s transform, .4s .3s opacity;
586 | cursor: pointer;
587 |
588 | &:hover {
589 | background: #fff;
590 | color: currentColor;
591 | }
592 | }
593 | .waves {
594 | position: absolute;
595 | bottom: 0;
596 | left: 0;
597 | width: 100%;
598 | height: 100%;
599 | overflow: hidden;
600 |
601 | transition: .4s transform ease;
602 | transform-origin: bottom center;
603 |
604 | @media (min-width: 500px) {
605 | border-radius: 5px;
606 | }
607 | }
608 | .wave {
609 | position: absolute;
610 | bottom: 0;
611 | left: 0;
612 | width: 100%;
613 | height: 100%;
614 | animation: wave 1s linear infinite;
615 |
616 | &--front {
617 | z-index: 2;
618 | color: $blue-light;
619 | }
620 |
621 | &--back {
622 | z-index: 1;
623 | color: $blue;
624 | animation-direction: reverse;
625 | }
626 | }
627 |
628 | .water {
629 | position: absolute;
630 | bottom: 0;
631 | left: 0;
632 | width: 100%;
633 | height: 80%;
634 | background: currentColor;
635 |
636 | svg {
637 | position: absolute;
638 | width: 100%;
639 | left: 0;
640 | right: 0;
641 | bottom: 99.9%;
642 | }
643 | }
644 | .water:first-of-type {
645 | transform: translate(-100%,0);
646 | }
647 |
648 | svg {
649 | fill: currentColor;
650 | }
651 |
652 | @keyframes wave{
653 | 0% {
654 | transform: translate3d(0,0,0);
655 | }
656 | 50% {
657 | transform: translate3d(50%,.5em,0);
658 | }
659 | 100% {
660 | transform: translate3d(100%,0,0);
661 | }
662 | }
663 |
664 | @keyframes pulse{
665 | 0% {
666 | transform: scale(1);
667 | }
668 | 50% {
669 | transform: scale(1.1);
670 | }
671 | 100% {
672 | transform: scale(1);
673 | }
674 | }
675 |
676 | @keyframes pulseAway {
677 | 0% {
678 | opacity: 0;
679 | transform: scale(.5);
680 | }
681 | 50% {
682 | opacity: 1;
683 | }
684 | 100% {
685 | transform: scale(1.4);
686 | }
687 | }
--------------------------------------------------------------------------------