` screen to add a new timer.
24 | - If the user presses the **Cancel** button on this screen, we should go back to the previous timer.
25 |
--------------------------------------------------------------------------------
/src/06/README.md:
--------------------------------------------------------------------------------
1 | # Exercise 06 - Hierarchical States
2 |
3 | In this exercise, we'll be exploring [hierarchical states](https://xstate.js.org/docs/guides/hierarchical.html) to add "overtime" logic. Instead of the machine going to an `expired` state, it should still have the same behavior as if it were running, but we should also be in some `running.overtime` state, so that we know that the time is already expired.
4 |
5 | We'll also use [forbidden transitions](https://xstate.js.org/docs/guides/transitions.html#forbidden-transitions) to tweak the behavior for these nested states.
6 |
7 | ## Goals
8 |
9 | - Add two hierarchical (nested) states to the `running` state:
10 | - `running.normal`, which is the normal timer countdown behavior, and the initial state
11 | - `running.overtime`, which is the expired timer countdown behavior that can also be reset.
12 | - Move the eventless transition from `running` into `running.normal`, and transition to the sibling `overtime` instead of `expired`, since this is what we want our new behavior to be.
13 | - Since we now want to allow `RESET` to transition to `idle` in both `running.overtime` and `paused`, move that transition to the parent, so that a `RESET` event would transition to `.idle`.
14 | - However, we don't want a `RESET` event to do anything in `running.normal`, so forbid that transition.
15 | - We also don't want `TOGGLE` to do anything in `running.overtime`, so forbid that transition as well.
16 |
--------------------------------------------------------------------------------
/src/08/foreignClockMachine.js:
--------------------------------------------------------------------------------
1 | import { assign, createMachine } from 'xstate';
2 |
3 | export const foreignClockMachine = createMachine({
4 | initial: 'loadingTimezones',
5 | context: {
6 | timezones: null,
7 | timezone: null,
8 | foreignTime: null,
9 | },
10 | states: {
11 | loadingTimezones: {
12 | on: {
13 | 'TIMEZONES.LOADED': {
14 | target: 'time',
15 | actions: assign({
16 | timezones: (_, e) => e.data,
17 | }),
18 | },
19 | },
20 | },
21 | time: {
22 | initial: 'noTimezoneSelected',
23 | states: {
24 | noTimezoneSelected: {},
25 | timezoneSelected: {
26 | on: {
27 | 'LOCAL.UPDATE': {
28 | actions: assign({
29 | foreignTime: (ctx, event) => {
30 | return new Date(event.time).toLocaleTimeString('en-US', {
31 | timeZone: ctx.timezone,
32 | });
33 | },
34 | }),
35 | },
36 | },
37 | },
38 | },
39 | on: {
40 | 'TIMEZONE.CHANGE': {
41 | target: '.timezoneSelected',
42 | actions: assign((ctx, e) => ({
43 | timezone: e.value,
44 | foreignTime: new Date().toLocaleTimeString('en-US', {
45 | timeZone: e.value,
46 | }),
47 | })),
48 | },
49 | },
50 | },
51 | },
52 | });
53 |
--------------------------------------------------------------------------------
/src/06/timerMachine.final.js:
--------------------------------------------------------------------------------
1 | import { createMachine, assign } from 'xstate';
2 |
3 | const timerExpired = (ctx) => ctx.elapsed >= ctx.duration;
4 |
5 | export const timerMachine = createMachine({
6 | initial: 'idle',
7 | context: {
8 | duration: 5,
9 | elapsed: 0,
10 | interval: 0.1,
11 | },
12 | states: {
13 | idle: {
14 | entry: assign({
15 | duration: 5,
16 | elapsed: 0,
17 | }),
18 | on: {
19 | TOGGLE: 'running',
20 | },
21 | },
22 | running: {
23 | initial: 'normal',
24 | states: {
25 | normal: {
26 | always: {
27 | target: 'overtime',
28 | cond: timerExpired,
29 | },
30 | on: {
31 | RESET: undefined,
32 | },
33 | },
34 | overtime: {
35 | on: {
36 | TOGGLE: undefined,
37 | },
38 | },
39 | },
40 | on: {
41 | TICK: {
42 | actions: assign({
43 | elapsed: (ctx) => ctx.elapsed + ctx.interval,
44 | }),
45 | },
46 | TOGGLE: 'paused',
47 | ADD_MINUTE: {
48 | actions: assign({
49 | duration: (ctx) => ctx.duration + 60,
50 | }),
51 | },
52 | },
53 | },
54 | paused: {
55 | on: {
56 | TOGGLE: 'running',
57 | },
58 | },
59 | },
60 | on: {
61 | RESET: {
62 | target: '.idle',
63 | },
64 | },
65 | });
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-finland-xstate-workshop",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@fortawesome/fontawesome-svg-core": "^1.2.28",
7 | "@fortawesome/free-solid-svg-icons": "^5.13.0",
8 | "@fortawesome/react-fontawesome": "^0.1.9",
9 | "@reach/tabs": "^0.11.2",
10 | "@testing-library/jest-dom": "^4.2.4",
11 | "@testing-library/react": "^11.1.1",
12 | "@testing-library/user-event": "^7.1.2",
13 | "@xstate/inspect": "^0.2.0",
14 | "@xstate/react": "^1.0.2",
15 | "@xstate/test": "^0.4.1",
16 | "react": "^16.13.1",
17 | "react-dom": "^16.13.1",
18 | "react-markdown": "^5.0.2",
19 | "react-query": "^2.25.2",
20 | "react-router": "^5.2.0",
21 | "react-router-dom": "^5.2.0",
22 | "react-scripts": "4.0.0",
23 | "xstate": "^4.14.0"
24 | },
25 | "scripts": {
26 | "start": "react-scripts start",
27 | "build": "react-scripts build",
28 | "test": "react-scripts test",
29 | "eject": "react-scripts eject"
30 | },
31 | "eslintConfig": {
32 | "extends": "react-app"
33 | },
34 | "browserslist": {
35 | "production": [
36 | ">0.2%",
37 | "not dead",
38 | "not op_mini all"
39 | ],
40 | "development": [
41 | "last 1 chrome version",
42 | "last 1 firefox version",
43 | "last 1 safari version"
44 | ]
45 | },
46 | "devDependencies": {
47 | "sass": "^1.42.1"
48 | },
49 | "engines": {
50 | "node": "16"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/08/timerAppMachine.final.js:
--------------------------------------------------------------------------------
1 | import { createMachine, assign, spawn } from 'xstate';
2 | import { createTimerMachine } from './timerMachine';
3 |
4 | export const timerAppMachine = createMachine({
5 | initial: 'new',
6 | context: {
7 | duration: 0,
8 | currentTimer: -1,
9 | timers: [],
10 | },
11 | states: {
12 | new: {
13 | on: {
14 | CANCEL: {
15 | target: 'timer',
16 | cond: (ctx) => ctx.timers.length > 0,
17 | },
18 | },
19 | },
20 | timer: {
21 | on: {
22 | DELETE: {
23 | actions: assign((ctx) => {
24 | const timers = ctx.timers.slice(0, -1);
25 | const currentTimer = timers.length - 1;
26 |
27 | return {
28 | timers,
29 | currentTimer,
30 | };
31 | }),
32 | target: 'deleting',
33 | },
34 | },
35 | },
36 | deleting: {
37 | always: [
38 | { target: 'new', cond: (ctx) => ctx.timers.length === 0 },
39 | { target: 'timer' },
40 | ],
41 | },
42 | },
43 | on: {
44 | ADD: {
45 | target: '.timer',
46 | actions: assign((ctx, event) => {
47 | const newTimer = spawn(createTimerMachine(event.duration));
48 |
49 | const timers = ctx.timers.concat(newTimer);
50 |
51 | return {
52 | timers,
53 | currentTimer: timers.length - 1,
54 | };
55 | }),
56 | },
57 | CREATE: 'new',
58 | SWITCH: {
59 | actions: assign({
60 | currentTimer: (_, event) => event.index,
61 | }),
62 | },
63 | },
64 | });
65 |
--------------------------------------------------------------------------------
/src/complete/timerAppMachine.js:
--------------------------------------------------------------------------------
1 | import { createMachine, assign, spawn } from 'xstate';
2 | import { createTimerMachine } from './timerMachine';
3 |
4 | export const timerAppMachine = createMachine({
5 | initial: 'new',
6 | context: {
7 | duration: 0,
8 | currentTimer: -1,
9 | timers: [],
10 | },
11 | states: {
12 | new: {
13 | on: {
14 | CANCEL: {
15 | target: 'timer',
16 | cond: (ctx) => ctx.timers.length > 0,
17 | },
18 | },
19 | },
20 | timer: {
21 | on: {
22 | DELETE: {
23 | actions: assign((ctx, e) => {
24 | const timers = ctx.timers.filter((_, i) => i !== e.index);
25 | const currentTimer = timers.length - 1;
26 |
27 | return {
28 | timers,
29 | currentTimer,
30 | };
31 | }),
32 | target: 'deleting',
33 | },
34 | },
35 | },
36 | deleting: {
37 | always: [
38 | { target: 'new', cond: (ctx) => ctx.timers.length === 0 },
39 | { target: 'timer' },
40 | ],
41 | },
42 | },
43 | on: {
44 | ADD: {
45 | target: '.timer',
46 | actions: assign((ctx, event) => {
47 | const newTimer = spawn(createTimerMachine(event.duration));
48 |
49 | const timers = ctx.timers.concat(newTimer);
50 |
51 | return {
52 | timers,
53 | currentTimer: timers.length - 1,
54 | };
55 | }),
56 | },
57 | CREATE: 'new',
58 | SWITCH: {
59 | actions: assign({
60 | currentTimer: (_, event) => event.index,
61 | }),
62 | },
63 | },
64 | });
65 |
--------------------------------------------------------------------------------
/src/07/timerMachine.js:
--------------------------------------------------------------------------------
1 | import { createMachine, assign } from 'xstate';
2 |
3 | const ticker = (context, event) => (callback) => {
4 | // This is the callback service creator.
5 | // Add the implementation details here.
6 | // ...
7 | };
8 |
9 | const timerExpired = (ctx) => ctx.elapsed >= ctx.duration;
10 |
11 | // https://xstate.js.org/viz/?gist=78fef4bd3ae520709ceaee62c0dd59cd
12 | export const timerMachine = createMachine({
13 | id: 'timer',
14 | initial: 'idle',
15 | context: {
16 | duration: 60,
17 | elapsed: 0,
18 | interval: 0.1,
19 | },
20 | states: {
21 | idle: {
22 | entry: assign({
23 | duration: 60,
24 | elapsed: 0,
25 | }),
26 | on: {
27 | TOGGLE: 'running',
28 | RESET: undefined,
29 | },
30 | },
31 | running: {
32 | // Invoke the callback service here.
33 | // ...
34 |
35 | initial: 'normal',
36 | states: {
37 | normal: {
38 | always: {
39 | target: 'overtime',
40 | cond: timerExpired,
41 | },
42 | on: {
43 | RESET: undefined,
44 | },
45 | },
46 | overtime: {
47 | on: {
48 | TOGGLE: undefined,
49 | },
50 | },
51 | },
52 | on: {
53 | TICK: {
54 | actions: assign({
55 | elapsed: (ctx) => ctx.elapsed + ctx.interval,
56 | }),
57 | },
58 | TOGGLE: 'paused',
59 | ADD_MINUTE: {
60 | actions: assign({
61 | duration: (ctx) => ctx.duration + 60,
62 | }),
63 | },
64 | },
65 | },
66 | paused: {
67 | on: { TOGGLE: 'running' },
68 | },
69 | },
70 | on: {
71 | RESET: {
72 | target: '.idle',
73 | },
74 | },
75 | });
76 |
--------------------------------------------------------------------------------
/src/08/NewTimer.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import { faPlay } from '@fortawesome/free-solid-svg-icons';
4 | import { useMachine } from '@xstate/react';
5 | import { newTimerMachine } from './newTimerMachine';
6 | import { useRef } from 'react';
7 |
8 | export const NewTimer = ({ onSubmit, onCancel }) => {
9 | const inputRef = useRef();
10 | const [state, send] = useMachine(newTimerMachine, {
11 | actions: {
12 | submit: (context) => {
13 | onSubmit(context.duration);
14 | },
15 | },
16 | });
17 |
18 | React.useEffect(() => {
19 | inputRef.current?.focus();
20 | }, [inputRef]);
21 |
22 | const { duration } = state.context;
23 |
24 | return (
25 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/src/complete/NewTimer.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import { faPlay } from '@fortawesome/free-solid-svg-icons';
4 | import { useMachine } from '@xstate/react';
5 | import { newTimerMachine } from './newTimerMachine';
6 | import { useRef } from 'react';
7 |
8 | export const NewTimer = ({ onSubmit, onCancel }) => {
9 | const inputRef = useRef();
10 | const [state, send] = useMachine(newTimerMachine, {
11 | actions: {
12 | submit: (context) => {
13 | onSubmit(context.duration);
14 | },
15 | },
16 | });
17 |
18 | React.useEffect(() => {
19 | inputRef.current?.focus();
20 | }, [inputRef]);
21 |
22 | const { duration } = state.context;
23 |
24 | return (
25 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/src/08/timerAppMachine.js:
--------------------------------------------------------------------------------
1 | import { createMachine, assign, spawn } from 'xstate';
2 | import { createTimerMachine } from './timerMachine';
3 |
4 | export const timerAppMachine = createMachine({
5 | initial: 'new',
6 | context: {
7 | duration: 0,
8 | currentTimer: -1,
9 | timers: [],
10 | },
11 | states: {
12 | new: {
13 | on: {
14 | CANCEL: {
15 | target: 'timer',
16 | cond: (ctx) => ctx.timers.length > 0,
17 | },
18 | },
19 | },
20 | timer: {
21 | on: {
22 | DELETE: {
23 | actions: assign((ctx) => {
24 | const timers = ctx.timers.slice(0, -1);
25 | const currentTimer = timers.length - 1;
26 |
27 | return {
28 | timers,
29 | currentTimer,
30 | };
31 | }),
32 | target: 'deleting',
33 | },
34 | },
35 | },
36 | deleting: {
37 | always: [
38 | { target: 'new', cond: (ctx) => ctx.timers.length === 0 },
39 | { target: 'timer' },
40 | ],
41 | },
42 | },
43 | on: {
44 | ADD: {
45 | // Uncomment this once you've added the spawn() code:
46 | // target: '.timer',
47 | actions: assign((ctx, event) => {
48 | // Spawn a new timerMachine here (using createTimerMachine)
49 | // and append this timer to context.timers
50 | // ...
51 |
52 | // Change the below line to return the updated context:
53 | // - `context.timers` should contain the appended spawned timer
54 | // - `context.currentTimer` should be the index of that spawned timer
55 | return ctx;
56 | }),
57 | },
58 | CREATE: 'new',
59 | SWITCH: {
60 | actions: assign({
61 | currentTimer: (_, event) => event.index,
62 | }),
63 | },
64 | },
65 | });
66 |
--------------------------------------------------------------------------------
/src/07/timerMachine.final.js:
--------------------------------------------------------------------------------
1 | import { createMachine, assign } from 'xstate';
2 |
3 | const ticker = (ctx) => (cb) => {
4 | const interval = setInterval(() => {
5 | cb('TICK');
6 | }, ctx.interval * 1000);
7 | return () => clearInterval(interval);
8 | };
9 |
10 | const timerExpired = (ctx) => ctx.elapsed >= ctx.duration;
11 |
12 | // https://xstate.js.org/viz/?gist=78fef4bd3ae520709ceaee62c0dd59cd
13 | export const timerMachine = createMachine({
14 | id: 'timer',
15 | initial: 'idle',
16 | context: {
17 | duration: 60,
18 | elapsed: 0,
19 | interval: 0.1,
20 | },
21 | states: {
22 | idle: {
23 | entry: assign({
24 | duration: 60,
25 | elapsed: 0,
26 | }),
27 | on: {
28 | TOGGLE: 'running',
29 | RESET: undefined,
30 | },
31 | },
32 | running: {
33 | invoke: {
34 | id: 'ticker', // only used for viz
35 | src: ticker,
36 | },
37 | initial: 'normal',
38 | states: {
39 | normal: {
40 | always: {
41 | target: 'overtime',
42 | cond: timerExpired,
43 | },
44 | on: {
45 | RESET: undefined,
46 | },
47 | },
48 | overtime: {
49 | on: {
50 | TOGGLE: undefined,
51 | },
52 | },
53 | },
54 | on: {
55 | TICK: {
56 | actions: assign({
57 | elapsed: (ctx) => ctx.elapsed + ctx.interval,
58 | }),
59 | },
60 | TOGGLE: 'paused',
61 | ADD_MINUTE: {
62 | actions: assign({
63 | duration: (ctx) => ctx.duration + 60,
64 | }),
65 | },
66 | },
67 | },
68 | paused: {
69 | on: { TOGGLE: 'running' },
70 | },
71 | },
72 | on: {
73 | RESET: {
74 | target: '.idle',
75 | },
76 | },
77 | });
78 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/01/Timer.final.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { faPlay, faPause } from '@fortawesome/free-solid-svg-icons';
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4 |
5 | import { useMachine } from '@xstate/react';
6 | import { ProgressCircle } from '../ProgressCircle';
7 |
8 | import { timerMachine } from './timerMachine.final';
9 |
10 | export const Timer = () => {
11 | const [state, send] = useMachine(timerMachine);
12 |
13 | const { duration, elapsed, interval } = {
14 | duration: 60,
15 | elapsed: 0,
16 | interval: 0.1,
17 | };
18 |
19 | return (
20 |
30 |
31 | Exercise 01 Solution
32 |
33 |
34 |
35 |
{state.value}
36 |
send({ type: 'TOGGLE' })}>
37 | {Math.ceil(duration - elapsed)}
38 |
39 |
40 | {state.value === 'paused' && (
41 |
42 | )}
43 |
44 |
45 |
46 | {state.value === 'running' && (
47 |
50 | )}
51 |
52 | {(state.value === 'paused' || state.value === 'idle') && (
53 |
56 | )}
57 |
58 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/src/08/ForeignClock.js:
--------------------------------------------------------------------------------
1 | import { useMachine, useService } from '@xstate/react';
2 | import { useContext, useEffect } from 'react';
3 | import { useQuery } from 'react-query';
4 | import { LocalTimeContext } from './Clock';
5 | import { foreignClockMachine } from './foreignClockMachine';
6 | import mockTimezones from './timezones.json';
7 |
8 | export function ForeignClock() {
9 | const localTimeService = useContext(LocalTimeContext);
10 | const [localTimeState] = useService(localTimeService);
11 | const [state, send] = useMachine(foreignClockMachine);
12 |
13 | const { data } = useQuery('timezones', () => {
14 | // return Promise.resolve(mockTimezones);
15 | return fetch('http://worldtimeapi.org/api/timezone').then((data) =>
16 | data.json()
17 | );
18 | });
19 |
20 | useEffect(() => {
21 | if (data) {
22 | send({
23 | type: 'TIMEZONES.LOADED',
24 | data,
25 | });
26 | }
27 | }, [data, send]);
28 |
29 | useEffect(() => {
30 | send({
31 | type: 'LOCAL.UPDATE',
32 | time: localTimeState.context.time,
33 | });
34 | }, [localTimeState, send]);
35 |
36 | const { timezones, foreignTime } = state.context;
37 |
38 | return (
39 |
40 | {(state.matches('timezonesLoaded') || timezones) && (
41 | <>
42 |
58 | {foreignTime || '--'}
59 | >
60 | )}
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/src/00/Timer.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useReducer } from 'react';
3 | import { faPlay, faPause } from '@fortawesome/free-solid-svg-icons';
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5 | import { ProgressCircle } from '../ProgressCircle';
6 |
7 | // Import the timer machine and its initial state:
8 | // import { ... } from './timerMachine';
9 |
10 | export const Timer = () => {
11 | const state = ''; // delete me - useReducer instead!
12 |
13 | const { duration, elapsed, interval } = {
14 | duration: 60,
15 | elapsed: 0,
16 | interval: 0.1,
17 | };
18 |
19 | return (
20 |
30 |
33 |
34 |
35 |
{state}
36 |
{
39 | // ...
40 | }}
41 | >
42 | {Math.ceil(duration - elapsed)}
43 |
44 |
45 |
52 |
53 |
54 |
55 |
63 |
64 |
72 |
73 |
74 | );
75 | };
76 |
--------------------------------------------------------------------------------
/src/08/timerMachine.js:
--------------------------------------------------------------------------------
1 | import { createMachine, assign } from 'xstate';
2 |
3 | const ticker = (ctx) => (sendBack) => {
4 | const interval = setInterval(() => {
5 | sendBack('TICK');
6 | }, ctx.interval * 1000);
7 |
8 | return () => clearInterval(interval);
9 | };
10 |
11 | const timerExpired = (ctx) => ctx.elapsed >= ctx.duration;
12 |
13 | // https://xstate.js.org/viz/?gist=78fef4bd3ae520709ceaee62c0dd59cd
14 | export const createTimerMachine = (duration) =>
15 | createMachine({
16 | id: 'timer',
17 | initial: 'running',
18 | context: {
19 | duration,
20 | elapsed: 0,
21 | interval: 0.1,
22 | },
23 | states: {
24 | idle: {
25 | entry: assign({
26 | duration,
27 | elapsed: 0,
28 | }),
29 | on: {
30 | TOGGLE: 'running',
31 | RESET: undefined,
32 | },
33 | },
34 | running: {
35 | invoke: {
36 | id: 'ticker', // only used for viz
37 | src: ticker,
38 | },
39 | initial: 'normal',
40 | states: {
41 | normal: {
42 | always: {
43 | target: 'overtime',
44 | cond: timerExpired,
45 | },
46 | on: {
47 | RESET: undefined,
48 | },
49 | },
50 | overtime: {
51 | on: {
52 | TOGGLE: undefined,
53 | },
54 | },
55 | },
56 | on: {
57 | TICK: {
58 | actions: assign({
59 | elapsed: (ctx) => ctx.elapsed + ctx.interval,
60 | }),
61 | },
62 | TOGGLE: 'paused',
63 | ADD_MINUTE: {
64 | actions: assign({
65 | duration: (ctx) => ctx.duration + 60,
66 | }),
67 | },
68 | },
69 | },
70 | paused: {
71 | on: { TOGGLE: 'running' },
72 | },
73 | },
74 | on: {
75 | RESET: {
76 | target: '.idle',
77 | },
78 | },
79 | });
80 |
--------------------------------------------------------------------------------
/src/02/Timer.final.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { faPlay, faPause } from '@fortawesome/free-solid-svg-icons';
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4 |
5 | import { useMachine } from '@xstate/react';
6 | import { timerMachine } from './timerMachine.final';
7 | import { ProgressCircle } from '../ProgressCircle';
8 |
9 | export const Timer = () => {
10 | const [state, send] = useMachine(timerMachine);
11 |
12 | const { duration, elapsed, interval } = state.context;
13 |
14 | return (
15 |
25 |
26 | Exercise 02 Solution
27 |
28 |
29 |
30 |
{state.value}
31 |
send({ type: 'TOGGLE' })}>
32 | {Math.ceil(duration - elapsed)}
33 |
34 |
35 | {state.value !== 'running' && (
36 |
37 | )}
38 |
39 | {state.value === 'running' && (
40 |
41 | )}
42 |
43 |
44 |
45 | {state.value === 'running' && (
46 |
49 | )}
50 |
51 | {(state.value === 'paused' || state.value === 'idle') && (
52 |
55 | )}
56 |
57 |
58 | );
59 | };
60 |
--------------------------------------------------------------------------------
/src/complete/timerMachine.js:
--------------------------------------------------------------------------------
1 | import { createMachine, assign } from 'xstate';
2 |
3 | const ticker = (ctx) => (sendBack) => {
4 | const interval = setInterval(() => {
5 | sendBack('TICK');
6 | }, ctx.interval * 1000);
7 |
8 | return () => clearInterval(interval);
9 | };
10 |
11 | const timerExpired = (ctx) => ctx.elapsed >= ctx.duration;
12 |
13 | // https://xstate.js.org/viz/?gist=78fef4bd3ae520709ceaee62c0dd59cd
14 | export const createTimerMachine = (duration) =>
15 | createMachine({
16 | id: 'timer',
17 | initial: 'running',
18 | context: {
19 | duration,
20 | elapsed: 0,
21 | interval: 0.1,
22 | },
23 | states: {
24 | idle: {
25 | entry: assign({
26 | duration,
27 | elapsed: 0,
28 | }),
29 | on: {
30 | TOGGLE: 'running',
31 | RESET: undefined,
32 | },
33 | },
34 | running: {
35 | invoke: {
36 | id: 'ticker', // only used for viz
37 | src: ticker,
38 | },
39 | initial: 'normal',
40 | states: {
41 | normal: {
42 | always: {
43 | target: 'overtime',
44 | cond: timerExpired,
45 | },
46 | on: {
47 | RESET: undefined,
48 | },
49 | },
50 | overtime: {
51 | on: {
52 | TOGGLE: undefined,
53 | },
54 | },
55 | },
56 | on: {
57 | TICK: {
58 | actions: assign({
59 | elapsed: (ctx) => ctx.elapsed + ctx.interval,
60 | }),
61 | },
62 | TOGGLE: 'paused',
63 | ADD_MINUTE: {
64 | actions: assign({
65 | duration: (ctx) => ctx.duration + 60,
66 | }),
67 | },
68 | },
69 | },
70 | paused: {
71 | on: { TOGGLE: 'running' },
72 | },
73 | },
74 | on: {
75 | RESET: {
76 | target: '.idle',
77 | },
78 | },
79 | });
80 |
--------------------------------------------------------------------------------
/src/00/Timer.final.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useReducer } from 'react';
3 | import { faPlay, faPause, faStop } from '@fortawesome/free-solid-svg-icons';
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5 |
6 | import { timerMachine, timerMachineConfig } from './timerMachine.final';
7 | import { ProgressCircle } from '../ProgressCircle';
8 |
9 | export const Timer = () => {
10 | const [state, dispatch] = useReducer(
11 | timerMachine,
12 | timerMachineConfig.initial
13 | );
14 |
15 | const { duration, elapsed, interval } = {
16 | duration: 60,
17 | elapsed: 0,
18 | interval: 0.1,
19 | };
20 |
21 | return (
22 |
32 |
33 | Exercise 00 Solution
34 |
35 |
36 |
37 |
{state}
38 |
dispatch({ type: 'TOGGLE' })}>
39 | {Math.ceil(duration - elapsed)}
40 |
41 |
42 | {state === 'paused' && (
43 |
44 | )}
45 |
46 |
47 |
48 | {state === 'running' && (
49 |
55 | )}
56 | {(state === 'paused' || state === 'idle') && (
57 |
63 | )}
64 |
65 |
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/src/02/Timer.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { faPlay, faPause } from '@fortawesome/free-solid-svg-icons';
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4 |
5 | import { useMachine } from '@xstate/react';
6 | import { timerMachine } from './timerMachine';
7 | import { ProgressCircle } from '../ProgressCircle';
8 |
9 | export const Timer = () => {
10 | const [state, send] = useMachine(timerMachine);
11 |
12 | // Use state.context instead
13 | const { duration, elapsed, interval } = {
14 | duration: 60,
15 | elapsed: 0,
16 | interval: 0.1,
17 | };
18 |
19 | return (
20 |
30 |
33 |
34 |
35 |
{state.value}
36 |
send({ type: 'TOGGLE' })}>
37 | {Math.ceil(duration - elapsed)}
38 |
39 |
40 | {state.value !== 'running' && (
41 |
42 | )}
43 |
44 |
51 |
52 |
53 |
54 | {state.value === 'running' && (
55 |
58 | )}
59 |
60 | {(state.value === 'paused' || state.value === 'idle') && (
61 |
64 | )}
65 |
66 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/src/03/Timer.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useEffect } from 'react';
3 | import { faPlay, faPause } from '@fortawesome/free-solid-svg-icons';
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5 |
6 | import { useMachine } from '@xstate/react';
7 | import { ProgressCircle } from '../ProgressCircle';
8 |
9 | import { timerMachine } from './timerMachine';
10 |
11 | export const Timer = () => {
12 | const [state, send] = useMachine(timerMachine);
13 |
14 | const { duration, elapsed, interval } = state.context;
15 |
16 | // Add a useEffect(...) here to send a TICK event on every `interval`
17 | // ...
18 |
19 | return (
20 |
30 |
33 |
34 |
35 |
{state.value}
36 |
send({ type: 'TOGGLE' })}>
37 | {Math.ceil(duration - elapsed)}
38 |
39 |
40 | {state.value !== 'running' && (
41 |
42 | )}
43 |
44 | {state.value === 'running' && (
45 |
46 | )}
47 |
48 |
49 |
50 | {state.value === 'running' && (
51 |
54 | )}
55 |
56 | {(state.value === 'paused' || state.value === 'idle') && (
57 |
60 | )}
61 |
62 |
63 | );
64 | };
65 |
--------------------------------------------------------------------------------
/src/complete/ForeignClock.js:
--------------------------------------------------------------------------------
1 | import { useMachine, useService } from '@xstate/react';
2 | import { useContext, useEffect } from 'react';
3 | import { useQuery } from 'react-query';
4 | import { LocalTimeContext } from './Clock';
5 | import { foreignClockMachine } from './foreignClockMachine';
6 | import mockTimezones from './timezones.json';
7 |
8 | export function ForeignClock() {
9 | const localTimeService = useContext(LocalTimeContext);
10 | const [localTimeState] = useService(localTimeService);
11 | const [state, send] = useMachine(foreignClockMachine);
12 |
13 | const { data } = useQuery('timezones', () => {
14 | // return Promise.resolve(mockTimezones);
15 | return fetch('http://worldtimeapi.org/api/timezone').then((data) =>
16 | data.json()
17 | );
18 | });
19 |
20 | useEffect(() => {
21 | if (data) {
22 | send({
23 | type: 'TIMEZONES.LOADED',
24 | data,
25 | });
26 | }
27 | }, [data, send]);
28 |
29 | useEffect(() => {
30 | send({
31 | type: 'LOCAL.UPDATE',
32 | time: localTimeState.context.time,
33 | });
34 | }, [localTimeState, send]);
35 |
36 | const { timezones, foreignTime, timezone } = state.context;
37 |
38 | const formattedTime = foreignTime?.toLocaleTimeString('en-US', {
39 | hour: '2-digit',
40 | minute: '2-digit',
41 | timeZone: timezone,
42 | });
43 |
44 | return (
45 |
46 | {(state.matches('timezonesLoaded') || timezones) && (
47 | <>
48 |
64 | {formattedTime || '--'}
65 | >
66 | )}
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/src/01/Timer.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { faPlay, faPause } from '@fortawesome/free-solid-svg-icons';
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4 |
5 | import { ProgressCircle } from '../ProgressCircle';
6 |
7 | // import { useMachine } from '@xstate/react';
8 | import { timerMachine } from './timerMachine';
9 |
10 | export const Timer = () => {
11 | const [state, send] = [{}, () => {}];
12 |
13 | const { duration, elapsed, interval } = {
14 | duration: 60,
15 | elapsed: 0,
16 | interval: 0.1,
17 | };
18 |
19 | return (
20 |
30 |
33 |
34 |
35 |
{state.value}
36 |
{
39 | // ...
40 | }}
41 | >
42 | {Math.ceil(duration - elapsed)}
43 |
44 |
45 | {state === 'paused' && (
46 |
53 | )}
54 |
55 |
56 |
57 | {state === 'running' && (
58 |
66 | )}
67 |
68 | {(state === 'paused' || state === 'idle') && (
69 |
77 | )}
78 |
79 |
80 | );
81 | };
82 |
--------------------------------------------------------------------------------
/src/05/Timer.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useEffect } from 'react';
3 | import { faPlay, faPause } from '@fortawesome/free-solid-svg-icons';
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5 |
6 | import { useMachine } from '@xstate/react';
7 | import { timerMachine } from './timerMachine';
8 | import { ProgressCircle } from '../ProgressCircle';
9 |
10 | export const Timer = () => {
11 | const [state, send] = useMachine(timerMachine);
12 |
13 | const { duration, elapsed, interval } = state.context;
14 |
15 | useEffect(() => {
16 | const intervalId = setInterval(() => {
17 | send('TICK');
18 | }, interval * 1000);
19 |
20 | return () => clearInterval(intervalId);
21 | }, []);
22 |
23 | return (
24 |
34 |
37 |
38 |
39 |
{state.value}
40 |
send({ type: 'TOGGLE' })}>
41 | {Math.ceil(duration - elapsed)}
42 |
43 |
44 | {state.value !== 'running' && (
45 |
46 | )}
47 |
48 | {state.value === 'running' && (
49 |
50 | )}
51 |
52 |
53 |
54 | {state.value === 'running' && (
55 |
58 | )}
59 |
60 | {(state.value === 'paused' || state.value === 'idle') && (
61 |
64 | )}
65 |
66 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/src/04/Timer.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useEffect } from 'react';
3 | import { faPlay, faPause } from '@fortawesome/free-solid-svg-icons';
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5 |
6 | import { useMachine } from '@xstate/react';
7 | import { ProgressCircle } from '../ProgressCircle';
8 |
9 | import { timerMachine } from './timerMachine';
10 |
11 | export const Timer = () => {
12 | const [state, send] = useMachine(timerMachine);
13 |
14 | const { duration, elapsed, interval } = state.context;
15 |
16 | useEffect(() => {
17 | const intervalId = setInterval(() => {
18 | send('TICK');
19 | }, interval * 1000);
20 |
21 | return () => clearInterval(intervalId);
22 | }, []);
23 |
24 | return (
25 |
35 |
38 |
39 |
40 |
{state.value}
41 |
send({ type: 'TOGGLE' })}>
42 | {Math.ceil(duration - elapsed)}
43 |
44 |
45 | {state.value !== 'running' && (
46 |
47 | )}
48 |
49 | {state.value === 'running' && (
50 |
51 | )}
52 |
53 |
54 |
55 | {state.value === 'running' && (
56 |
59 | )}
60 |
61 | {(state.value === 'paused' || state.value === 'idle') && (
62 |
65 | )}
66 |
67 |
68 | );
69 | };
70 |
--------------------------------------------------------------------------------
/src/07/Timer.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { faPlay, faPause, faStop } from '@fortawesome/free-solid-svg-icons';
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4 | import { useMachine } from '@xstate/react';
5 |
6 | import { ProgressCircle } from '../ProgressCircle';
7 | import { timerMachine } from './timerMachine';
8 |
9 | export const Timer = () => {
10 | const [state, send] = useMachine(timerMachine);
11 |
12 | const { duration, elapsed, interval } = state.context;
13 |
14 | return (
15 |
25 |
28 |
29 |
30 |
{state.toStrings().slice(-1)}
31 |
send('TOGGLE')}>
32 | {Math.ceil(duration - elapsed)}
33 |
34 |
35 | {!state.matches({ running: 'normal' }) && (
36 |
37 | )}
38 |
39 | {state.matches({ running: 'normal' }) && (
40 |
41 | )}
42 |
43 |
44 |
45 | {state.matches({ running: 'normal' }) && (
46 |
49 | )}
50 | {state.matches({ running: 'overtime' }) && (
51 |
54 | )}
55 | {(state.matches('paused') || state.matches('idle')) && (
56 |
59 | )}
60 |
61 |
62 | );
63 | };
64 |
--------------------------------------------------------------------------------
/src/05/Timer.final.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useEffect } from 'react';
3 | import { faPlay, faPause } from '@fortawesome/free-solid-svg-icons';
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5 |
6 | import { useMachine } from '@xstate/react';
7 | import { timerMachine } from './timerMachine.final';
8 | import { ProgressCircle } from '../ProgressCircle';
9 |
10 | export const Timer = () => {
11 | const [state, send] = useMachine(timerMachine);
12 |
13 | const { duration, elapsed, interval } = state.context;
14 |
15 | useEffect(() => {
16 | const intervalId = setInterval(() => {
17 | send('TICK');
18 | }, interval * 1000);
19 |
20 | return () => clearInterval(intervalId);
21 | }, []);
22 |
23 | return (
24 |
34 |
35 | Exercise 05 Solution
36 |
37 |
38 |
39 |
{state.value}
40 |
send({ type: 'TOGGLE' })}>
41 | {Math.ceil(duration - elapsed)}
42 |
43 |
44 | {state.value !== 'running' && (
45 |
46 | )}
47 |
48 | {state.value === 'running' && (
49 |
50 | )}
51 |
52 |
53 |
54 | {state.value === 'running' && (
55 |
58 | )}
59 |
60 | {(state.value === 'paused' || state.value === 'idle') && (
61 |
64 | )}
65 |
66 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/src/04/Timer.final.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useEffect } from 'react';
3 | import { faPlay, faPause } from '@fortawesome/free-solid-svg-icons';
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5 |
6 | import { useMachine } from '@xstate/react';
7 | import { ProgressCircle } from '../ProgressCircle';
8 |
9 | import { timerMachine } from './timerMachine.final';
10 |
11 | export const Timer = () => {
12 | const [state, send] = useMachine(timerMachine);
13 |
14 | const { duration, elapsed, interval } = state.context;
15 |
16 | useEffect(() => {
17 | const intervalId = setInterval(() => {
18 | send('TICK');
19 | }, interval * 1000);
20 |
21 | return () => clearInterval(intervalId);
22 | }, []);
23 |
24 | return (
25 |
35 |
36 | Exercise 04 Solution
37 |
38 |
39 |
40 |
{state.value}
41 |
send({ type: 'TOGGLE' })}>
42 | {Math.ceil(duration - elapsed)}
43 |
44 |
45 | {state.value !== 'running' && (
46 |
47 | )}
48 |
49 | {state.value === 'running' && (
50 |
51 | )}
52 |
53 |
54 |
55 | {state.value === 'running' && (
56 |
59 | )}
60 |
61 | {(state.value === 'paused' || state.value === 'idle') && (
62 |
65 | )}
66 |
67 |
68 | );
69 | };
70 |
--------------------------------------------------------------------------------
/src/07/Timer.final.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { faPlay, faPause, faStop } from '@fortawesome/free-solid-svg-icons';
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4 | import { useMachine } from '@xstate/react';
5 |
6 | import { ProgressCircle } from '../ProgressCircle';
7 | import { timerMachine } from './timerMachine.final';
8 |
9 | export const Timer = () => {
10 | const [state, send] = useMachine(timerMachine);
11 |
12 | const { duration, elapsed, interval } = state.context;
13 |
14 | return (
15 |
25 |
26 | Exercise 07 Solution
27 |
28 |
29 |
30 |
{state.toStrings().slice(-1)}
31 |
send('TOGGLE')}>
32 | {Math.ceil(duration - elapsed)}
33 |
34 |
35 | {!state.matches({ running: 'normal' }) && (
36 |
37 | )}
38 |
39 | {state.matches({ running: 'normal' }) && (
40 |
41 | )}
42 |
43 |
44 |
45 | {state.matches({ running: 'normal' }) && (
46 |
49 | )}
50 | {state.matches({ running: 'overtime' }) && (
51 |
54 | )}
55 | {(state.matches('paused') || state.matches('idle')) && (
56 |
59 | )}
60 |
61 |
62 | );
63 | };
64 |
--------------------------------------------------------------------------------
/src/03/Timer.final.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useEffect } from 'react';
3 | import { faPlay, faPause } from '@fortawesome/free-solid-svg-icons';
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5 |
6 | import { useMachine } from '@xstate/react';
7 | import { ProgressCircle } from '../ProgressCircle';
8 |
9 | import { timerMachine } from './timerMachine.final';
10 |
11 | export const Timer = () => {
12 | const [state, send] = useMachine(timerMachine);
13 |
14 | const { duration, elapsed, interval } = state.context;
15 |
16 | useEffect(() => {
17 | if (state.value === 'running') {
18 | const intervalId = setInterval(() => {
19 | send('TICK');
20 | }, interval * 1000);
21 |
22 | return () => clearInterval(intervalId);
23 | }
24 | }, [state.value]);
25 |
26 | return (
27 |
37 |
38 | Exercise 03 Solution
39 |
40 |
41 |
42 |
{state.value}
43 |
send({ type: 'TOGGLE' })}>
44 | {Math.ceil(duration - elapsed)}
45 |
46 |
47 | {state.value !== 'running' && (
48 |
49 | )}
50 |
51 | {state.value === 'running' && (
52 |
53 | )}
54 |
55 |
56 |
57 | {state.value === 'running' && (
58 |
61 | )}
62 |
63 | {(state.value === 'paused' || state.value === 'idle') && (
64 |
67 | )}
68 |
69 |
70 | );
71 | };
72 |
--------------------------------------------------------------------------------
/src/06/Timer.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useEffect } from 'react';
3 | import { faPlay, faPause, faStop } from '@fortawesome/free-solid-svg-icons';
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5 |
6 | import { useMachine } from '@xstate/react';
7 | import { ProgressCircle } from '../ProgressCircle';
8 |
9 | import { timerMachine } from './timerMachine';
10 |
11 | export const Timer = () => {
12 | const [state, send] = useMachine(timerMachine);
13 |
14 | const { duration, elapsed, interval } = state.context;
15 |
16 | useEffect(() => {
17 | const intervalId = setInterval(() => {
18 | send('TICK');
19 | }, interval * 1000);
20 |
21 | return () => clearInterval(intervalId);
22 | }, []);
23 |
24 | return (
25 |
35 |
38 |
39 |
40 |
{state.toStrings().slice(-1)}
41 |
send('TOGGLE')}>
42 | {Math.ceil(duration - elapsed)}
43 |
44 |
45 | {!state.matches({ running: 'normal' }) && (
46 |
47 | )}
48 |
49 | {state.matches({ running: 'normal' }) && (
50 |
51 | )}
52 |
53 |
54 |
55 | {state.matches({ running: 'normal' }) && (
56 |
59 | )}
60 | {state.matches({ running: 'overtime' }) && (
61 |
64 | )}
65 | {(state.matches('paused') || state.matches('idle')) && (
66 |
69 | )}
70 |
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/src/06/Timer.final.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useEffect } from 'react';
3 | import { faPlay, faPause, faStop } from '@fortawesome/free-solid-svg-icons';
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5 |
6 | import { useMachine } from '@xstate/react';
7 | import { ProgressCircle } from '../ProgressCircle';
8 |
9 | import { timerMachine } from './timerMachine.final';
10 |
11 | export const Timer = () => {
12 | const [state, send] = useMachine(timerMachine);
13 |
14 | const { duration, elapsed, interval } = state.context;
15 |
16 | useEffect(() => {
17 | const intervalId = setInterval(() => {
18 | send('TICK');
19 | }, interval * 1000);
20 |
21 | return () => clearInterval(intervalId);
22 | }, []);
23 |
24 | return (
25 |
35 |
36 | Exercise 06 Solution
37 |
38 |
39 |
40 |
{state.toStrings().slice(-1)}
41 |
send('TOGGLE')}>
42 | {Math.ceil(duration - elapsed)}
43 |
44 |
45 | {!state.matches({ running: 'normal' }) && (
46 |
47 | )}
48 |
49 | {state.matches({ running: 'normal' }) && (
50 |
51 | )}
52 |
53 |
54 |
55 | {state.matches({ running: 'normal' }) && (
56 |
59 | )}
60 | {state.matches({ running: 'overtime' }) && (
61 |
64 | )}
65 | {(state.matches('paused') || state.matches('idle')) && (
66 |
69 | )}
70 |
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import * as React from 'react';
3 | import { createModel } from '@xstate/test';
4 | import { createMachine, assign } from 'xstate';
5 | import { render, fireEvent, cleanup } from '@testing-library/react';
6 | import { App } from './complete/App';
7 |
8 | const testTimerAppMachine = createMachine({
9 | initial: 'newTimer',
10 | context: {
11 | value: '',
12 | },
13 | states: {
14 | newTimer: {
15 | initial: 'noTimers',
16 | states: {
17 | noTimers: {},
18 | afterDeleted: {},
19 | adding: {},
20 | },
21 | on: {
22 | CHANGE: {
23 | actions: assign({
24 | value: 124,
25 | }),
26 | },
27 | PLAY: {
28 | cond: (ctx) => ctx.value > 0,
29 | target: 'timer',
30 | },
31 | },
32 | meta: {
33 | test: async ({ getByTestId }) => {
34 | getByTestId('new-timer'); // [data-testid="new-timer"]
35 | },
36 | },
37 | },
38 | timer: {
39 | on: {
40 | DELETE: 'newTimer.afterDeleted',
41 | ADD: 'newTimer.adding',
42 | },
43 | meta: {
44 | test: async ({ getByText }, state) => {
45 | getByText(/XState Minute Timer/i);
46 | },
47 | },
48 | },
49 | },
50 | });
51 |
52 | const testTimerAppModel = createModel(testTimerAppMachine).withEvents({
53 | CHANGE: {
54 | exec: async ({ getByTitle }) => {
55 | const input = getByTitle(/Duration/i);
56 |
57 | fireEvent.change(input, { target: { value: '124' } });
58 | },
59 | },
60 | PLAY: {
61 | exec: async ({ getByTitle }) => {
62 | const addButton = getByTitle(/Start .* timer/i);
63 |
64 | fireEvent.click(addButton);
65 | },
66 | },
67 | DELETE: {
68 | exec: async ({ getByTitle }) => {
69 | const deleteButton = getByTitle(/Delete/i);
70 |
71 | fireEvent.click(deleteButton);
72 | },
73 | },
74 | ADD: {
75 | exec: async ({ getByTitle }) => {
76 | const addButton = getByTitle(/Add/i);
77 |
78 | fireEvent.click(addButton);
79 | },
80 | },
81 | });
82 |
83 | describe('something', () => {
84 | const testPlans = testTimerAppModel.getSimplePathPlans();
85 |
86 | testPlans.forEach((plan) => {
87 | describe(plan.description, () => {
88 | afterEach(cleanup);
89 |
90 | plan.paths.forEach((path) => {
91 | it(path.description, () => {
92 | const rendered = render();
93 |
94 | return path.test(rendered);
95 | });
96 | });
97 | });
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/src/08/App.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useMachine } from '@xstate/react';
3 | import { Tabs, Tab, TabList, TabPanels, TabPanel } from '@reach/tabs';
4 | import { NewTimer } from './NewTimer';
5 | import { Timer } from './Timer';
6 | import { Clock } from './Clock';
7 | import { timerAppMachine } from './timerAppMachine';
8 |
9 | export const App = () => {
10 | const [state, send] = useMachine(timerAppMachine);
11 | const { timers } = state.context;
12 |
13 | return (
14 |
20 |
21 | Clock
22 | Timer
23 |
24 |
25 |
26 |
27 |
28 |
29 | {
31 | send({ type: 'ADD', duration });
32 | }}
33 | onCancel={
34 | timers.length
35 | ? () => {
36 | send('CANCEL');
37 | }
38 | : undefined
39 | }
40 | key={state.toStrings().join(' ')}
41 | />
42 |
43 | {state.context.timers.map((timer, i) => {
44 | return (
45 | {
49 | send('DELETE');
50 | }}
51 | onAdd={() => {
52 | send('CREATE');
53 | }}
54 | data-active={i === state.context.currentTimer || undefined}
55 | />
56 | );
57 | })}
58 |
59 |
60 | {state.context.timers.map((_, index) => {
61 | return (
62 |
{
69 | send({ type: 'SWITCH', index: index });
70 | }}
71 | >
72 | );
73 | })}
74 |
75 |
76 |
77 |
78 | );
79 | };
80 |
--------------------------------------------------------------------------------
/src/08/Timer.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { faPlay, faPause, faStop } from '@fortawesome/free-solid-svg-icons';
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4 | import { useService } from '@xstate/react';
5 |
6 | import { ProgressCircle } from '../ProgressCircle';
7 |
8 | export const Timer = ({ onDelete, onAdd, timerRef, ...attrs }) => {
9 | const [state, send] = useService(timerRef);
10 |
11 | const { duration, elapsed, interval } = state.context;
12 |
13 | return (
14 |
25 |
26 | XState Minute Timer
27 |
28 |
29 |
30 |
{state.toStrings().slice(-1)}
31 |
send('TOGGLE')}>
32 | {Math.ceil(duration - elapsed)}
33 |
34 |
35 | {!state.matches({ running: 'normal' }) && (
36 |
37 | )}
38 |
39 | {state.matches({ running: 'normal' }) && (
40 |
41 | )}
42 |
43 |
44 |
45 |
54 | {state.matches({ running: 'normal' }) && (
55 |
58 | )}
59 | {state.matches({ running: 'overtime' }) && (
60 |
63 | )}
64 | {(state.matches('paused') || state.matches('idle')) && (
65 |
68 | )}
69 |
78 |
79 |
80 | );
81 | };
82 |
--------------------------------------------------------------------------------
/src/complete/Timer.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { faPlay, faPause, faStop } from '@fortawesome/free-solid-svg-icons';
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4 | import { useService } from '@xstate/react';
5 | import { ProgressCircle } from '../ProgressCircle';
6 |
7 | export const Timer = ({ onDelete, onAdd, timerRef, ...attrs }) => {
8 | const [state, send] = useService(timerRef);
9 |
10 | const { duration, elapsed, interval } = state.context;
11 |
12 | return (
13 |
24 |
25 | XState Minute Timer
26 |
27 |
28 |
29 |
{state.toStrings().slice(-1)}
30 |
send('TOGGLE')}>
31 | {Math.ceil(duration - elapsed)}
32 |
33 |
34 | {!state.matches({ running: 'normal' }) && (
35 |
36 | )}
37 |
38 | {state.matches({ running: 'normal' }) && (
39 |
40 | )}
41 |
42 |
43 |
44 |
53 | {state.matches({ running: 'normal' }) && (
54 |
57 | )}
58 | {state.matches({ running: 'overtime' }) && (
59 |
62 | )}
63 | {(state.matches('paused') || state.matches('idle')) && (
64 |
67 | )}
68 |
77 |
78 |
79 | );
80 | };
81 |
--------------------------------------------------------------------------------
/src/complete/App.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useMachine } from '@xstate/react';
3 | import { Tabs, Tab, TabList, TabPanels, TabPanel } from '@reach/tabs';
4 | import { NewTimer } from './NewTimer';
5 | import { Timer } from './Timer';
6 | import { Clock } from './Clock';
7 | import { timerAppMachine } from './timerAppMachine';
8 | import { inspect } from '@xstate/inspect';
9 |
10 | // inspect({
11 | // iframe: false,
12 | // });
13 |
14 | export const App = () => {
15 | const [state, send] = useMachine(timerAppMachine, { devTools: true });
16 | const { timers } = state.context;
17 |
18 | return (
19 |
25 |
26 | Clock
27 | Timer
28 |
29 |
30 |
31 |
32 |
33 |
34 | {
36 | send({ type: 'ADD', duration });
37 | }}
38 | onCancel={
39 | timers.length
40 | ? () => {
41 | send('CANCEL');
42 | }
43 | : undefined
44 | }
45 | key={state.toStrings().join(' ')}
46 | />
47 |
48 | {state.context.timers.map((timer, i) => {
49 | return (
50 | {
54 | send({ type: 'DELETE', index: i });
55 | }}
56 | onAdd={() => {
57 | send('CREATE');
58 | }}
59 | data-active={i === state.context.currentTimer || undefined}
60 | />
61 | );
62 | })}
63 |
64 |
65 | {state.context.timers.map((_, index) => {
66 | return (
67 |
{
74 | send({ type: 'SWITCH', index: index });
75 | }}
76 | >
77 | );
78 | })}
79 |
80 |
81 |
82 |
83 | );
84 | };
85 |
--------------------------------------------------------------------------------
/src/Workshop.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
3 |
4 | // To see the final versions of each exercise, append .final to the path; e.g.:
5 | // import { Timer as Timer00 } from './00/Timer.final';
6 |
7 | import { Timer as Timer00 } from './00/Timer';
8 | import { Timer as Timer01 } from './01/Timer';
9 | import { Timer as Timer02 } from './02/Timer';
10 | import { Timer as Timer03 } from './03/Timer';
11 | import { Timer as Timer04 } from './04/Timer';
12 | import { Timer as Timer05 } from './05/Timer';
13 | import { Timer as Timer06 } from './06/Timer';
14 | import { Timer as Timer07 } from './07/Timer';
15 | import { App as App08 } from './08/App';
16 | import { App as AppComplete } from './complete/App';
17 | import { ScratchApp } from './scratch';
18 | import { Exercise } from './Exercise';
19 |
20 | function getMarkdownLink(exercise) {
21 | return require(`./${exercise}/README.md`).default;
22 | }
23 |
24 | function App() {
25 | return (
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | );
86 | }
87 |
88 | export default App;
89 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' },
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready
134 | .then(registration => {
135 | registration.unregister();
136 | })
137 | .catch(error => {
138 | console.error(error.message);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Lato:wght@400&display=swap');
2 | @import './styles/exercise.scss';
3 | @import './styles/dots.scss';
4 | @import './styles/newTimer.scss';
5 | @import './styles/scratch.scss';
6 | @import './styles/alarm.scss';
7 |
8 | *,
9 | *:before,
10 | *:after {
11 | box-sizing: border-box;
12 | position: relative;
13 | }
14 |
15 | [hidden] {
16 | display: none !important;
17 | }
18 |
19 | html,
20 | body,
21 | #root {
22 | height: 100%;
23 | width: 100%;
24 | margin: 0;
25 | padding: 0;
26 | font-family: 'Lato', sans-serif;
27 | font-weight: 400;
28 | font-size: 16px;
29 | }
30 |
31 | :root {
32 | --color-primary: #5192e6;
33 | --color-primary-light: hsla(214, 75%, 72%, 1);
34 | --color-secondary: #8b9096;
35 | --color-gray: #373b41;
36 | --color-dark-gray: #25272a;
37 | }
38 |
39 | #root {
40 | background-color: #18191c;
41 | color: white;
42 | }
43 |
44 | .app {
45 | height: 100%;
46 | width: 100%;
47 | display: grid;
48 | grid-template-columns: 1fr;
49 | grid-template-rows: auto 1fr;
50 | overflow: hidden;
51 |
52 | > .app-panels {
53 | grid-template-rows: 2 / -1;
54 | display: grid;
55 | grid-template-columns: 1fr;
56 | grid-template-rows: 1fr;
57 | overflow: scroll;
58 |
59 | > .app-panel {
60 | grid-area: 1 / 1;
61 | display: grid;
62 | grid-template-columns: 1fr;
63 | grid-template-rows: 1fr;
64 | overflow: scroll;
65 | }
66 |
67 | .timers,
68 | .timer,
69 | [data-screen] {
70 | grid-area: 1 / 1;
71 | transition: transform 0.6s ease-in-out, opacity 0.3s ease-out;
72 | }
73 |
74 | .dots {
75 | position: absolute;
76 | top: 0;
77 | right: 0;
78 | width: 5rem;
79 | height: 100%;
80 | }
81 |
82 | .timer:not([data-active]) {
83 | display: none;
84 | }
85 | }
86 |
87 | &[data-state='timer'] {
88 | [data-screen='new-timer'] {
89 | transform: translateY(-50%);
90 | opacity: 0;
91 | }
92 |
93 | .timer {
94 | animation: slide-up 0.6s ease-in-out both;
95 |
96 | @keyframes slide-up {
97 | from {
98 | opacity: 0;
99 | transform: translateY(50%);
100 | }
101 | 50% {
102 | opacity: 1;
103 | }
104 | }
105 | }
106 | }
107 | }
108 |
109 | .app-tabs {
110 | display: flex;
111 | flex-direction: row;
112 | justify-content: stretch;
113 | background-color: var(--color-dark-gray);
114 | }
115 |
116 | .app-tab {
117 | appearance: none;
118 | background: transparent;
119 | flex-grow: 1;
120 | display: flex;
121 | justify-content: center;
122 | align-items: center;
123 | padding: 1rem 0;
124 | border-bottom: 0.25rem solid transparent;
125 |
126 | &[data-selected] {
127 | border-color: var(--color-primary);
128 | }
129 | }
130 |
131 | input[type='number']::-webkit-inner-spin-button,
132 | input[type='number']::-webkit-outer-spin-button {
133 | -webkit-appearance: none;
134 | margin: 0;
135 | }
136 |
137 | .timer,
138 | .clock,
139 | [data-screen] {
140 | display: grid;
141 | grid-template-rows: 20vh 1fr 25vh;
142 | grid-template-areas: 'header' 'main' 'actions';
143 | grid-row-gap: 1rem;
144 | }
145 |
146 | .timer {
147 | --progress: calc((var(--elapsed)) / var(--duration));
148 | height: 100%;
149 | overflow: auto;
150 |
151 | > header {
152 | grid-area: header;
153 | }
154 |
155 | > .circles,
156 | > .display {
157 | grid-area: main;
158 | align-self: center;
159 | justify-self: center;
160 | }
161 |
162 | > .actions {
163 | grid-area: actions;
164 | }
165 |
166 | &[data-state~='idle'] {
167 | .progress {
168 | transition-duration: 0.3s;
169 | }
170 | }
171 |
172 | &[data-state~='paused'] {
173 | .elapsed {
174 | animation: pulse 1s steps(1) infinite;
175 | }
176 | }
177 |
178 | &[data-state~='running.overtime'],
179 | &[data-state~='expired'] {
180 | circle {
181 | animation: pulse 1s steps(1) infinite;
182 | }
183 | .elapsed {
184 | color: white;
185 | }
186 | }
187 |
188 | &[data-state~='running.overtime'],
189 | &[data-state~='expired'] {
190 | --progress: 1;
191 | }
192 |
193 | &[data-state~='running.normal'],
194 | &[data-state~='paused'] {
195 | --progress: calc((var(--elapsed) + var(--interval)) / var(--duration));
196 | }
197 | }
198 |
199 | .actions {
200 | display: grid;
201 | grid-template-columns: repeat(3, 1fr);
202 | grid-template-areas: 'left center right';
203 | align-items: center;
204 | justify-items: center;
205 |
206 | > button:not(.transparent) {
207 | grid-area: center;
208 | appearance: none;
209 | background-color: var(--color-primary);
210 | color: white;
211 | height: 4rem;
212 | width: 4rem;
213 | border-radius: 4rem;
214 | border: none;
215 | font-size: 1.5rem;
216 | font-weight: bold;
217 | transition: background-color 0.3s ease;
218 |
219 | &:hover {
220 | background-color: var(--color-primary-light);
221 | }
222 | }
223 |
224 | > button.transparent {
225 | width: auto;
226 | background: transparent;
227 | padding: 1rem;
228 | }
229 | }
230 |
231 | button {
232 | appearance: none;
233 | color: white;
234 | background: rgba(white, 0.2);
235 | padding: 0.5rem 1rem;
236 | border: none;
237 | font-size: 1.5rem;
238 | cursor: pointer; // I know, I know
239 | transition: transform 0.3s;
240 |
241 | &:focus {
242 | outline: none; // I KNOW DO NOT @ ME
243 | }
244 |
245 | &:disabled {
246 | opacity: 0.2;
247 | }
248 |
249 | &[hidden] {
250 | transform: scale(0);
251 | }
252 | }
253 |
254 | .display {
255 | display: flex;
256 | flex-direction: column;
257 | justify-content: center;
258 | align-items: center;
259 | }
260 |
261 | .elapsed {
262 | color: var(--color-primary);
263 | font-size: 5rem;
264 | padding: 1rem;
265 | transition: color 0.3s ease;
266 | cursor: pointer;
267 |
268 | &:hover {
269 | color: white;
270 | }
271 | }
272 |
273 | .label {
274 | color: var(--color-secondary);
275 | }
276 |
277 | .controls {
278 | display: flex;
279 | flex-direction: row;
280 |
281 | > button {
282 | appearance: none;
283 | border: none;
284 | background: transparent;
285 | color: white;
286 | font-size: 1rem;
287 | }
288 | }
289 |
290 | .circles {
291 | transform: rotate(-0.25turn);
292 | height: 100%;
293 | width: 100%;
294 | pointer-events: none;
295 | }
296 |
297 | circle {
298 | stroke: white;
299 | stroke-linejoin: round;
300 | stroke-linecap: round;
301 | stroke-width: 1px;
302 | }
303 |
304 | .progress {
305 | --interval-duration: calc(var(--interval) * 1s);
306 | transition: stroke-dashoffset var(--interval-duration) linear;
307 | stroke: var(--color-primary);
308 | stroke-dashoffset: calc(1px - var(--progress) * -1px);
309 | stroke-dasharray: 1 1;
310 | stroke-width: 1.1px;
311 | }
312 |
313 | @keyframes pulse {
314 | from,
315 | to {
316 | opacity: 0;
317 | }
318 | 50% {
319 | opacity: 1;
320 | }
321 | }
322 |
323 | header {
324 | display: flex;
325 | justify-content: center;
326 | align-items: center;
327 |
328 | a {
329 | color: white;
330 | opacity: 0.5;
331 | transition: opacity 0.3s ease;
332 |
333 | &:hover {
334 | opacity: 1;
335 | }
336 | }
337 | }
338 |
339 | // .....
340 |
341 | .local {
342 | align-self: self-end;
343 | }
344 |
345 | .localTime {
346 | font-size: 5rem;
347 | font-weight: normal;
348 | margin: 0;
349 | text-align: center;
350 | color: var(--color-primary);
351 | }
352 |
353 | .localDate {
354 | font-size: 1.5rem;
355 | display: block;
356 | text-align: center;
357 | }
358 |
359 | .foreignItem {
360 | display: grid;
361 | padding: 1rem;
362 | grid-template-columns: 1fr 1fr;
363 | grid-template-rows: 1fr;
364 | grid-column-gap: 1rem;
365 | grid-row-gap: 0.5rem;
366 | grid-template-areas: 'city time';
367 | border-top: 1px solid #333;
368 |
369 | > .foreignCity {
370 | grid-area: city;
371 | }
372 | > .foreignTime {
373 | grid-area: time;
374 | }
375 | > .foreignDetails {
376 | grid-area: details;
377 | }
378 | }
379 |
380 | .foreignCity {
381 | appearance: none;
382 | background: transparent;
383 | color: white;
384 | border: 1px solid white;
385 | padding: 0 1rem;
386 | justify-self: self-start;
387 | display: inline-block;
388 |
389 | &:focus {
390 | outline: none;
391 | }
392 | }
393 |
394 | .foreignTime {
395 | font-size: 2rem;
396 | align-self: center;
397 | }
398 |
--------------------------------------------------------------------------------
/src/08/timezones.json:
--------------------------------------------------------------------------------
1 | [
2 | "Africa/Abidjan",
3 | "Africa/Accra",
4 | "Africa/Algiers",
5 | "Africa/Bissau",
6 | "Africa/Cairo",
7 | "Africa/Casablanca",
8 | "Africa/Ceuta",
9 | "Africa/El_Aaiun",
10 | "Africa/Johannesburg",
11 | "Africa/Juba",
12 | "Africa/Khartoum",
13 | "Africa/Lagos",
14 | "Africa/Maputo",
15 | "Africa/Monrovia",
16 | "Africa/Nairobi",
17 | "Africa/Ndjamena",
18 | "Africa/Sao_Tome",
19 | "Africa/Tripoli",
20 | "Africa/Tunis",
21 | "Africa/Windhoek",
22 | "America/Adak",
23 | "America/Anchorage",
24 | "America/Araguaina",
25 | "America/Argentina/Buenos_Aires",
26 | "America/Argentina/Catamarca",
27 | "America/Argentina/Cordoba",
28 | "America/Argentina/Jujuy",
29 | "America/Argentina/La_Rioja",
30 | "America/Argentina/Mendoza",
31 | "America/Argentina/Rio_Gallegos",
32 | "America/Argentina/Salta",
33 | "America/Argentina/San_Juan",
34 | "America/Argentina/San_Luis",
35 | "America/Argentina/Tucuman",
36 | "America/Argentina/Ushuaia",
37 | "America/Asuncion",
38 | "America/Atikokan",
39 | "America/Bahia",
40 | "America/Bahia_Banderas",
41 | "America/Barbados",
42 | "America/Belem",
43 | "America/Belize",
44 | "America/Blanc-Sablon",
45 | "America/Boa_Vista",
46 | "America/Bogota",
47 | "America/Boise",
48 | "America/Cambridge_Bay",
49 | "America/Campo_Grande",
50 | "America/Cancun",
51 | "America/Caracas",
52 | "America/Cayenne",
53 | "America/Chicago",
54 | "America/Chihuahua",
55 | "America/Costa_Rica",
56 | "America/Creston",
57 | "America/Cuiaba",
58 | "America/Curacao",
59 | "America/Danmarkshavn",
60 | "America/Dawson",
61 | "America/Dawson_Creek",
62 | "America/Denver",
63 | "America/Detroit",
64 | "America/Edmonton",
65 | "America/Eirunepe",
66 | "America/El_Salvador",
67 | "America/Fort_Nelson",
68 | "America/Fortaleza",
69 | "America/Glace_Bay",
70 | "America/Goose_Bay",
71 | "America/Grand_Turk",
72 | "America/Guatemala",
73 | "America/Guayaquil",
74 | "America/Guyana",
75 | "America/Halifax",
76 | "America/Havana",
77 | "America/Hermosillo",
78 | "America/Indiana/Indianapolis",
79 | "America/Indiana/Knox",
80 | "America/Indiana/Marengo",
81 | "America/Indiana/Petersburg",
82 | "America/Indiana/Tell_City",
83 | "America/Indiana/Vevay",
84 | "America/Indiana/Vincennes",
85 | "America/Indiana/Winamac",
86 | "America/Inuvik",
87 | "America/Iqaluit",
88 | "America/Jamaica",
89 | "America/Juneau",
90 | "America/Kentucky/Louisville",
91 | "America/Kentucky/Monticello",
92 | "America/La_Paz",
93 | "America/Lima",
94 | "America/Los_Angeles",
95 | "America/Maceio",
96 | "America/Managua",
97 | "America/Manaus",
98 | "America/Martinique",
99 | "America/Matamoros",
100 | "America/Mazatlan",
101 | "America/Menominee",
102 | "America/Merida",
103 | "America/Metlakatla",
104 | "America/Mexico_City",
105 | "America/Miquelon",
106 | "America/Moncton",
107 | "America/Monterrey",
108 | "America/Montevideo",
109 | "America/Nassau",
110 | "America/New_York",
111 | "America/Nipigon",
112 | "America/Nome",
113 | "America/Noronha",
114 | "America/North_Dakota/Beulah",
115 | "America/North_Dakota/Center",
116 | "America/North_Dakota/New_Salem",
117 | "America/Nuuk",
118 | "America/Ojinaga",
119 | "America/Panama",
120 | "America/Pangnirtung",
121 | "America/Paramaribo",
122 | "America/Phoenix",
123 | "America/Port-au-Prince",
124 | "America/Port_of_Spain",
125 | "America/Porto_Velho",
126 | "America/Puerto_Rico",
127 | "America/Punta_Arenas",
128 | "America/Rainy_River",
129 | "America/Rankin_Inlet",
130 | "America/Recife",
131 | "America/Regina",
132 | "America/Resolute",
133 | "America/Rio_Branco",
134 | "America/Santarem",
135 | "America/Santiago",
136 | "America/Santo_Domingo",
137 | "America/Sao_Paulo",
138 | "America/Scoresbysund",
139 | "America/Sitka",
140 | "America/St_Johns",
141 | "America/Swift_Current",
142 | "America/Tegucigalpa",
143 | "America/Thule",
144 | "America/Thunder_Bay",
145 | "America/Tijuana",
146 | "America/Toronto",
147 | "America/Vancouver",
148 | "America/Whitehorse",
149 | "America/Winnipeg",
150 | "America/Yakutat",
151 | "America/Yellowknife",
152 | "Antarctica/Casey",
153 | "Antarctica/Davis",
154 | "Antarctica/DumontDUrville",
155 | "Antarctica/Macquarie",
156 | "Antarctica/Mawson",
157 | "Antarctica/Palmer",
158 | "Antarctica/Rothera",
159 | "Antarctica/Syowa",
160 | "Antarctica/Troll",
161 | "Antarctica/Vostok",
162 | "Asia/Almaty",
163 | "Asia/Amman",
164 | "Asia/Anadyr",
165 | "Asia/Aqtau",
166 | "Asia/Aqtobe",
167 | "Asia/Ashgabat",
168 | "Asia/Atyrau",
169 | "Asia/Baghdad",
170 | "Asia/Baku",
171 | "Asia/Bangkok",
172 | "Asia/Barnaul",
173 | "Asia/Beirut",
174 | "Asia/Bishkek",
175 | "Asia/Brunei",
176 | "Asia/Chita",
177 | "Asia/Choibalsan",
178 | "Asia/Colombo",
179 | "Asia/Damascus",
180 | "Asia/Dhaka",
181 | "Asia/Dili",
182 | "Asia/Dubai",
183 | "Asia/Dushanbe",
184 | "Asia/Famagusta",
185 | "Asia/Gaza",
186 | "Asia/Hebron",
187 | "Asia/Ho_Chi_Minh",
188 | "Asia/Hong_Kong",
189 | "Asia/Hovd",
190 | "Asia/Irkutsk",
191 | "Asia/Jakarta",
192 | "Asia/Jayapura",
193 | "Asia/Jerusalem",
194 | "Asia/Kabul",
195 | "Asia/Kamchatka",
196 | "Asia/Karachi",
197 | "Asia/Kathmandu",
198 | "Asia/Khandyga",
199 | "Asia/Kolkata",
200 | "Asia/Krasnoyarsk",
201 | "Asia/Kuala_Lumpur",
202 | "Asia/Kuching",
203 | "Asia/Macau",
204 | "Asia/Magadan",
205 | "Asia/Makassar",
206 | "Asia/Manila",
207 | "Asia/Nicosia",
208 | "Asia/Novokuznetsk",
209 | "Asia/Novosibirsk",
210 | "Asia/Omsk",
211 | "Asia/Oral",
212 | "Asia/Pontianak",
213 | "Asia/Pyongyang",
214 | "Asia/Qatar",
215 | "Asia/Qostanay",
216 | "Asia/Qyzylorda",
217 | "Asia/Riyadh",
218 | "Asia/Sakhalin",
219 | "Asia/Samarkand",
220 | "Asia/Seoul",
221 | "Asia/Shanghai",
222 | "Asia/Singapore",
223 | "Asia/Srednekolymsk",
224 | "Asia/Taipei",
225 | "Asia/Tashkent",
226 | "Asia/Tbilisi",
227 | "Asia/Tehran",
228 | "Asia/Thimphu",
229 | "Asia/Tokyo",
230 | "Asia/Tomsk",
231 | "Asia/Ulaanbaatar",
232 | "Asia/Urumqi",
233 | "Asia/Ust-Nera",
234 | "Asia/Vladivostok",
235 | "Asia/Yakutsk",
236 | "Asia/Yangon",
237 | "Asia/Yekaterinburg",
238 | "Asia/Yerevan",
239 | "Atlantic/Azores",
240 | "Atlantic/Bermuda",
241 | "Atlantic/Canary",
242 | "Atlantic/Cape_Verde",
243 | "Atlantic/Faroe",
244 | "Atlantic/Madeira",
245 | "Atlantic/Reykjavik",
246 | "Atlantic/South_Georgia",
247 | "Atlantic/Stanley",
248 | "Australia/Adelaide",
249 | "Australia/Brisbane",
250 | "Australia/Broken_Hill",
251 | "Australia/Currie",
252 | "Australia/Darwin",
253 | "Australia/Eucla",
254 | "Australia/Hobart",
255 | "Australia/Lindeman",
256 | "Australia/Lord_Howe",
257 | "Australia/Melbourne",
258 | "Australia/Perth",
259 | "Australia/Sydney",
260 | "CET",
261 | "CST6CDT",
262 | "EET",
263 | "EST",
264 | "EST5EDT",
265 | "Etc/GMT",
266 | "Etc/GMT+1",
267 | "Etc/GMT+10",
268 | "Etc/GMT+11",
269 | "Etc/GMT+12",
270 | "Etc/GMT+2",
271 | "Etc/GMT+3",
272 | "Etc/GMT+4",
273 | "Etc/GMT+5",
274 | "Etc/GMT+6",
275 | "Etc/GMT+7",
276 | "Etc/GMT+8",
277 | "Etc/GMT+9",
278 | "Etc/GMT-1",
279 | "Etc/GMT-10",
280 | "Etc/GMT-11",
281 | "Etc/GMT-12",
282 | "Etc/GMT-13",
283 | "Etc/GMT-14",
284 | "Etc/GMT-2",
285 | "Etc/GMT-3",
286 | "Etc/GMT-4",
287 | "Etc/GMT-5",
288 | "Etc/GMT-6",
289 | "Etc/GMT-7",
290 | "Etc/GMT-8",
291 | "Etc/GMT-9",
292 | "Etc/UTC",
293 | "Europe/Amsterdam",
294 | "Europe/Andorra",
295 | "Europe/Astrakhan",
296 | "Europe/Athens",
297 | "Europe/Belgrade",
298 | "Europe/Berlin",
299 | "Europe/Brussels",
300 | "Europe/Bucharest",
301 | "Europe/Budapest",
302 | "Europe/Chisinau",
303 | "Europe/Copenhagen",
304 | "Europe/Dublin",
305 | "Europe/Gibraltar",
306 | "Europe/Helsinki",
307 | "Europe/Istanbul",
308 | "Europe/Kaliningrad",
309 | "Europe/Kiev",
310 | "Europe/Kirov",
311 | "Europe/Lisbon",
312 | "Europe/London",
313 | "Europe/Luxembourg",
314 | "Europe/Madrid",
315 | "Europe/Malta",
316 | "Europe/Minsk",
317 | "Europe/Monaco",
318 | "Europe/Moscow",
319 | "Europe/Oslo",
320 | "Europe/Paris",
321 | "Europe/Prague",
322 | "Europe/Riga",
323 | "Europe/Rome",
324 | "Europe/Samara",
325 | "Europe/Saratov",
326 | "Europe/Simferopol",
327 | "Europe/Sofia",
328 | "Europe/Stockholm",
329 | "Europe/Tallinn",
330 | "Europe/Tirane",
331 | "Europe/Ulyanovsk",
332 | "Europe/Uzhgorod",
333 | "Europe/Vienna",
334 | "Europe/Vilnius",
335 | "Europe/Volgograd",
336 | "Europe/Warsaw",
337 | "Europe/Zaporozhye",
338 | "Europe/Zurich",
339 | "HST",
340 | "Indian/Chagos",
341 | "Indian/Christmas",
342 | "Indian/Cocos",
343 | "Indian/Kerguelen",
344 | "Indian/Mahe",
345 | "Indian/Maldives",
346 | "Indian/Mauritius",
347 | "Indian/Reunion",
348 | "MET",
349 | "MST",
350 | "MST7MDT",
351 | "PST8PDT",
352 | "Pacific/Apia",
353 | "Pacific/Auckland",
354 | "Pacific/Bougainville",
355 | "Pacific/Chatham",
356 | "Pacific/Chuuk",
357 | "Pacific/Easter",
358 | "Pacific/Efate",
359 | "Pacific/Enderbury",
360 | "Pacific/Fakaofo",
361 | "Pacific/Fiji",
362 | "Pacific/Funafuti",
363 | "Pacific/Galapagos",
364 | "Pacific/Gambier",
365 | "Pacific/Guadalcanal",
366 | "Pacific/Guam",
367 | "Pacific/Honolulu",
368 | "Pacific/Kiritimati",
369 | "Pacific/Kosrae",
370 | "Pacific/Kwajalein",
371 | "Pacific/Majuro",
372 | "Pacific/Marquesas",
373 | "Pacific/Nauru",
374 | "Pacific/Niue",
375 | "Pacific/Norfolk",
376 | "Pacific/Noumea",
377 | "Pacific/Pago_Pago",
378 | "Pacific/Palau",
379 | "Pacific/Pitcairn",
380 | "Pacific/Pohnpei",
381 | "Pacific/Port_Moresby",
382 | "Pacific/Rarotonga",
383 | "Pacific/Tahiti",
384 | "Pacific/Tarawa",
385 | "Pacific/Tongatapu",
386 | "Pacific/Wake",
387 | "Pacific/Wallis",
388 | "WET"
389 | ]
390 |
--------------------------------------------------------------------------------
/src/complete/timezones.json:
--------------------------------------------------------------------------------
1 | [
2 | "Africa/Abidjan",
3 | "Africa/Accra",
4 | "Africa/Algiers",
5 | "Africa/Bissau",
6 | "Africa/Cairo",
7 | "Africa/Casablanca",
8 | "Africa/Ceuta",
9 | "Africa/El_Aaiun",
10 | "Africa/Johannesburg",
11 | "Africa/Juba",
12 | "Africa/Khartoum",
13 | "Africa/Lagos",
14 | "Africa/Maputo",
15 | "Africa/Monrovia",
16 | "Africa/Nairobi",
17 | "Africa/Ndjamena",
18 | "Africa/Sao_Tome",
19 | "Africa/Tripoli",
20 | "Africa/Tunis",
21 | "Africa/Windhoek",
22 | "America/Adak",
23 | "America/Anchorage",
24 | "America/Araguaina",
25 | "America/Argentina/Buenos_Aires",
26 | "America/Argentina/Catamarca",
27 | "America/Argentina/Cordoba",
28 | "America/Argentina/Jujuy",
29 | "America/Argentina/La_Rioja",
30 | "America/Argentina/Mendoza",
31 | "America/Argentina/Rio_Gallegos",
32 | "America/Argentina/Salta",
33 | "America/Argentina/San_Juan",
34 | "America/Argentina/San_Luis",
35 | "America/Argentina/Tucuman",
36 | "America/Argentina/Ushuaia",
37 | "America/Asuncion",
38 | "America/Atikokan",
39 | "America/Bahia",
40 | "America/Bahia_Banderas",
41 | "America/Barbados",
42 | "America/Belem",
43 | "America/Belize",
44 | "America/Blanc-Sablon",
45 | "America/Boa_Vista",
46 | "America/Bogota",
47 | "America/Boise",
48 | "America/Cambridge_Bay",
49 | "America/Campo_Grande",
50 | "America/Cancun",
51 | "America/Caracas",
52 | "America/Cayenne",
53 | "America/Chicago",
54 | "America/Chihuahua",
55 | "America/Costa_Rica",
56 | "America/Creston",
57 | "America/Cuiaba",
58 | "America/Curacao",
59 | "America/Danmarkshavn",
60 | "America/Dawson",
61 | "America/Dawson_Creek",
62 | "America/Denver",
63 | "America/Detroit",
64 | "America/Edmonton",
65 | "America/Eirunepe",
66 | "America/El_Salvador",
67 | "America/Fort_Nelson",
68 | "America/Fortaleza",
69 | "America/Glace_Bay",
70 | "America/Goose_Bay",
71 | "America/Grand_Turk",
72 | "America/Guatemala",
73 | "America/Guayaquil",
74 | "America/Guyana",
75 | "America/Halifax",
76 | "America/Havana",
77 | "America/Hermosillo",
78 | "America/Indiana/Indianapolis",
79 | "America/Indiana/Knox",
80 | "America/Indiana/Marengo",
81 | "America/Indiana/Petersburg",
82 | "America/Indiana/Tell_City",
83 | "America/Indiana/Vevay",
84 | "America/Indiana/Vincennes",
85 | "America/Indiana/Winamac",
86 | "America/Inuvik",
87 | "America/Iqaluit",
88 | "America/Jamaica",
89 | "America/Juneau",
90 | "America/Kentucky/Louisville",
91 | "America/Kentucky/Monticello",
92 | "America/La_Paz",
93 | "America/Lima",
94 | "America/Los_Angeles",
95 | "America/Maceio",
96 | "America/Managua",
97 | "America/Manaus",
98 | "America/Martinique",
99 | "America/Matamoros",
100 | "America/Mazatlan",
101 | "America/Menominee",
102 | "America/Merida",
103 | "America/Metlakatla",
104 | "America/Mexico_City",
105 | "America/Miquelon",
106 | "America/Moncton",
107 | "America/Monterrey",
108 | "America/Montevideo",
109 | "America/Nassau",
110 | "America/New_York",
111 | "America/Nipigon",
112 | "America/Nome",
113 | "America/Noronha",
114 | "America/North_Dakota/Beulah",
115 | "America/North_Dakota/Center",
116 | "America/North_Dakota/New_Salem",
117 | "America/Nuuk",
118 | "America/Ojinaga",
119 | "America/Panama",
120 | "America/Pangnirtung",
121 | "America/Paramaribo",
122 | "America/Phoenix",
123 | "America/Port-au-Prince",
124 | "America/Port_of_Spain",
125 | "America/Porto_Velho",
126 | "America/Puerto_Rico",
127 | "America/Punta_Arenas",
128 | "America/Rainy_River",
129 | "America/Rankin_Inlet",
130 | "America/Recife",
131 | "America/Regina",
132 | "America/Resolute",
133 | "America/Rio_Branco",
134 | "America/Santarem",
135 | "America/Santiago",
136 | "America/Santo_Domingo",
137 | "America/Sao_Paulo",
138 | "America/Scoresbysund",
139 | "America/Sitka",
140 | "America/St_Johns",
141 | "America/Swift_Current",
142 | "America/Tegucigalpa",
143 | "America/Thule",
144 | "America/Thunder_Bay",
145 | "America/Tijuana",
146 | "America/Toronto",
147 | "America/Vancouver",
148 | "America/Whitehorse",
149 | "America/Winnipeg",
150 | "America/Yakutat",
151 | "America/Yellowknife",
152 | "Antarctica/Casey",
153 | "Antarctica/Davis",
154 | "Antarctica/DumontDUrville",
155 | "Antarctica/Macquarie",
156 | "Antarctica/Mawson",
157 | "Antarctica/Palmer",
158 | "Antarctica/Rothera",
159 | "Antarctica/Syowa",
160 | "Antarctica/Troll",
161 | "Antarctica/Vostok",
162 | "Asia/Almaty",
163 | "Asia/Amman",
164 | "Asia/Anadyr",
165 | "Asia/Aqtau",
166 | "Asia/Aqtobe",
167 | "Asia/Ashgabat",
168 | "Asia/Atyrau",
169 | "Asia/Baghdad",
170 | "Asia/Baku",
171 | "Asia/Bangkok",
172 | "Asia/Barnaul",
173 | "Asia/Beirut",
174 | "Asia/Bishkek",
175 | "Asia/Brunei",
176 | "Asia/Chita",
177 | "Asia/Choibalsan",
178 | "Asia/Colombo",
179 | "Asia/Damascus",
180 | "Asia/Dhaka",
181 | "Asia/Dili",
182 | "Asia/Dubai",
183 | "Asia/Dushanbe",
184 | "Asia/Famagusta",
185 | "Asia/Gaza",
186 | "Asia/Hebron",
187 | "Asia/Ho_Chi_Minh",
188 | "Asia/Hong_Kong",
189 | "Asia/Hovd",
190 | "Asia/Irkutsk",
191 | "Asia/Jakarta",
192 | "Asia/Jayapura",
193 | "Asia/Jerusalem",
194 | "Asia/Kabul",
195 | "Asia/Kamchatka",
196 | "Asia/Karachi",
197 | "Asia/Kathmandu",
198 | "Asia/Khandyga",
199 | "Asia/Kolkata",
200 | "Asia/Krasnoyarsk",
201 | "Asia/Kuala_Lumpur",
202 | "Asia/Kuching",
203 | "Asia/Macau",
204 | "Asia/Magadan",
205 | "Asia/Makassar",
206 | "Asia/Manila",
207 | "Asia/Nicosia",
208 | "Asia/Novokuznetsk",
209 | "Asia/Novosibirsk",
210 | "Asia/Omsk",
211 | "Asia/Oral",
212 | "Asia/Pontianak",
213 | "Asia/Pyongyang",
214 | "Asia/Qatar",
215 | "Asia/Qostanay",
216 | "Asia/Qyzylorda",
217 | "Asia/Riyadh",
218 | "Asia/Sakhalin",
219 | "Asia/Samarkand",
220 | "Asia/Seoul",
221 | "Asia/Shanghai",
222 | "Asia/Singapore",
223 | "Asia/Srednekolymsk",
224 | "Asia/Taipei",
225 | "Asia/Tashkent",
226 | "Asia/Tbilisi",
227 | "Asia/Tehran",
228 | "Asia/Thimphu",
229 | "Asia/Tokyo",
230 | "Asia/Tomsk",
231 | "Asia/Ulaanbaatar",
232 | "Asia/Urumqi",
233 | "Asia/Ust-Nera",
234 | "Asia/Vladivostok",
235 | "Asia/Yakutsk",
236 | "Asia/Yangon",
237 | "Asia/Yekaterinburg",
238 | "Asia/Yerevan",
239 | "Atlantic/Azores",
240 | "Atlantic/Bermuda",
241 | "Atlantic/Canary",
242 | "Atlantic/Cape_Verde",
243 | "Atlantic/Faroe",
244 | "Atlantic/Madeira",
245 | "Atlantic/Reykjavik",
246 | "Atlantic/South_Georgia",
247 | "Atlantic/Stanley",
248 | "Australia/Adelaide",
249 | "Australia/Brisbane",
250 | "Australia/Broken_Hill",
251 | "Australia/Currie",
252 | "Australia/Darwin",
253 | "Australia/Eucla",
254 | "Australia/Hobart",
255 | "Australia/Lindeman",
256 | "Australia/Lord_Howe",
257 | "Australia/Melbourne",
258 | "Australia/Perth",
259 | "Australia/Sydney",
260 | "CET",
261 | "CST6CDT",
262 | "EET",
263 | "EST",
264 | "EST5EDT",
265 | "Etc/GMT",
266 | "Etc/GMT+1",
267 | "Etc/GMT+10",
268 | "Etc/GMT+11",
269 | "Etc/GMT+12",
270 | "Etc/GMT+2",
271 | "Etc/GMT+3",
272 | "Etc/GMT+4",
273 | "Etc/GMT+5",
274 | "Etc/GMT+6",
275 | "Etc/GMT+7",
276 | "Etc/GMT+8",
277 | "Etc/GMT+9",
278 | "Etc/GMT-1",
279 | "Etc/GMT-10",
280 | "Etc/GMT-11",
281 | "Etc/GMT-12",
282 | "Etc/GMT-13",
283 | "Etc/GMT-14",
284 | "Etc/GMT-2",
285 | "Etc/GMT-3",
286 | "Etc/GMT-4",
287 | "Etc/GMT-5",
288 | "Etc/GMT-6",
289 | "Etc/GMT-7",
290 | "Etc/GMT-8",
291 | "Etc/GMT-9",
292 | "Etc/UTC",
293 | "Europe/Amsterdam",
294 | "Europe/Andorra",
295 | "Europe/Astrakhan",
296 | "Europe/Athens",
297 | "Europe/Belgrade",
298 | "Europe/Berlin",
299 | "Europe/Brussels",
300 | "Europe/Bucharest",
301 | "Europe/Budapest",
302 | "Europe/Chisinau",
303 | "Europe/Copenhagen",
304 | "Europe/Dublin",
305 | "Europe/Gibraltar",
306 | "Europe/Helsinki",
307 | "Europe/Istanbul",
308 | "Europe/Kaliningrad",
309 | "Europe/Kiev",
310 | "Europe/Kirov",
311 | "Europe/Lisbon",
312 | "Europe/London",
313 | "Europe/Luxembourg",
314 | "Europe/Madrid",
315 | "Europe/Malta",
316 | "Europe/Minsk",
317 | "Europe/Monaco",
318 | "Europe/Moscow",
319 | "Europe/Oslo",
320 | "Europe/Paris",
321 | "Europe/Prague",
322 | "Europe/Riga",
323 | "Europe/Rome",
324 | "Europe/Samara",
325 | "Europe/Saratov",
326 | "Europe/Simferopol",
327 | "Europe/Sofia",
328 | "Europe/Stockholm",
329 | "Europe/Tallinn",
330 | "Europe/Tirane",
331 | "Europe/Ulyanovsk",
332 | "Europe/Uzhgorod",
333 | "Europe/Vienna",
334 | "Europe/Vilnius",
335 | "Europe/Volgograd",
336 | "Europe/Warsaw",
337 | "Europe/Zaporozhye",
338 | "Europe/Zurich",
339 | "HST",
340 | "Indian/Chagos",
341 | "Indian/Christmas",
342 | "Indian/Cocos",
343 | "Indian/Kerguelen",
344 | "Indian/Mahe",
345 | "Indian/Maldives",
346 | "Indian/Mauritius",
347 | "Indian/Reunion",
348 | "MET",
349 | "MST",
350 | "MST7MDT",
351 | "PST8PDT",
352 | "Pacific/Apia",
353 | "Pacific/Auckland",
354 | "Pacific/Bougainville",
355 | "Pacific/Chatham",
356 | "Pacific/Chuuk",
357 | "Pacific/Easter",
358 | "Pacific/Efate",
359 | "Pacific/Enderbury",
360 | "Pacific/Fakaofo",
361 | "Pacific/Fiji",
362 | "Pacific/Funafuti",
363 | "Pacific/Galapagos",
364 | "Pacific/Gambier",
365 | "Pacific/Guadalcanal",
366 | "Pacific/Guam",
367 | "Pacific/Honolulu",
368 | "Pacific/Kiritimati",
369 | "Pacific/Kosrae",
370 | "Pacific/Kwajalein",
371 | "Pacific/Majuro",
372 | "Pacific/Marquesas",
373 | "Pacific/Nauru",
374 | "Pacific/Niue",
375 | "Pacific/Norfolk",
376 | "Pacific/Noumea",
377 | "Pacific/Pago_Pago",
378 | "Pacific/Palau",
379 | "Pacific/Pitcairn",
380 | "Pacific/Pohnpei",
381 | "Pacific/Port_Moresby",
382 | "Pacific/Rarotonga",
383 | "Pacific/Tahiti",
384 | "Pacific/Tarawa",
385 | "Pacific/Tongatapu",
386 | "Pacific/Wake",
387 | "Pacific/Wallis",
388 | "WET"
389 | ]
390 |
--------------------------------------------------------------------------------