diff --git a/package-lock.json b/package-lock.json
index e75776d..bf00415 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1323,6 +1323,24 @@
"vite": "^4.2.0 || ^5.0.0"
}
},
+ "node_modules/@xstate/react": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/@xstate/react/-/react-4.1.3.tgz",
+ "integrity": "sha512-zhE+ZfrcCR87bu71Rkh5Z5ruZBivR/7uD/dkelzJqjQdI45IZc9DqTI8lL4Cg5+VN2p5k86KxDsusqW1kW11Tg==",
+ "dependencies": {
+ "use-isomorphic-layout-effect": "^1.1.2",
+ "use-sync-external-store": "^1.2.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "xstate": "^5.18.2"
+ },
+ "peerDependenciesMeta": {
+ "xstate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
@@ -4384,6 +4402,19 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-isomorphic-layout-effect": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
+ "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
@@ -4624,6 +4655,15 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
+ "node_modules/xstate": {
+ "version": "5.18.2",
+ "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.18.2.tgz",
+ "integrity": "sha512-hab5VOe29D0agy8/7dH1lGw+7kilRQyXwpaChoMu4fe6rDP+nsHYhDYKfS2O4iXE7myA98TW6qMEudj/8NXEkA==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/xstate"
+ }
+ },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -4823,10 +4863,13 @@
}
},
"packages/xstate": {
+ "name": "@timo/xstate",
"version": "1.0.0",
"dependencies": {
+ "@xstate/react": "^4.1.3",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "xstate": "^5.18.2"
},
"devDependencies": {
"@types/react": "^18.2.64",
diff --git a/packages/jotai/src/App.jsx b/packages/jotai/src/App.jsx
index 9abc093..e45e1d9 100644
--- a/packages/jotai/src/App.jsx
+++ b/packages/jotai/src/App.jsx
@@ -13,7 +13,6 @@ import Router from './components/Router';
const routes = [
{ path: '/', name: 'Entries' },
{ path: '/login', name: 'Login' },
- { path: '/register', name: 'Register' },
{ path: '/new', name: 'NewEntry' },
{ path: '/profile', name: 'Profile' }
];
diff --git a/packages/mobx/src/App.jsx b/packages/mobx/src/App.jsx
index 1632b33..5596517 100644
--- a/packages/mobx/src/App.jsx
+++ b/packages/mobx/src/App.jsx
@@ -16,7 +16,6 @@ import Profile from './routes/Profile/Profile';
const routes = [
{ path: '/', name: 'Entries' },
{ path: '/login', name: 'Login' },
- { path: '/register', name: 'Register' },
{ path: '/new', name: 'NewEntry' },
{ path: '/profile', name: 'Profile' }
];
diff --git a/packages/react/src/App.jsx b/packages/react/src/App.jsx
index a945235..fbcbd95 100644
--- a/packages/react/src/App.jsx
+++ b/packages/react/src/App.jsx
@@ -13,7 +13,6 @@ import Profile from './routes/Profile/Profile';
const routes = [
{ path: '/', name: 'Entries' },
{ path: '/login', name: 'Login' },
- { path: '/register', name: 'Register' },
{ path: '/new', name: 'NewEntry' },
{ path: '/profile', name: 'Profile' }
];
diff --git a/packages/tanstack-query/src/App.jsx b/packages/tanstack-query/src/App.jsx
index 77d9680..1795471 100644
--- a/packages/tanstack-query/src/App.jsx
+++ b/packages/tanstack-query/src/App.jsx
@@ -17,7 +17,6 @@ import Profile from './routes/Profile/Profile';
const routes = [
{ path: '/', name: 'Entries' },
{ path: '/login', name: 'Login' },
- { path: '/register', name: 'Register' },
{ path: '/new', name: 'NewEntry' },
{ path: '/profile', name: 'Profile' }
];
diff --git a/packages/valtio/src/App.jsx b/packages/valtio/src/App.jsx
index 1632b33..5596517 100644
--- a/packages/valtio/src/App.jsx
+++ b/packages/valtio/src/App.jsx
@@ -16,7 +16,6 @@ import Profile from './routes/Profile/Profile';
const routes = [
{ path: '/', name: 'Entries' },
{ path: '/login', name: 'Login' },
- { path: '/register', name: 'Register' },
{ path: '/new', name: 'NewEntry' },
{ path: '/profile', name: 'Profile' }
];
diff --git a/packages/xstate/package.json b/packages/xstate/package.json
index 210ad62..f7bc784 100644
--- a/packages/xstate/package.json
+++ b/packages/xstate/package.json
@@ -9,8 +9,10 @@
"preview": "vite preview"
},
"dependencies": {
+ "@xstate/react": "^4.1.3",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "xstate": "^5.18.2"
},
"devDependencies": {
"@types/react": "^18.2.64",
diff --git a/packages/xstate/src/App.jsx b/packages/xstate/src/App.jsx
index a945235..b4910a9 100644
--- a/packages/xstate/src/App.jsx
+++ b/packages/xstate/src/App.jsx
@@ -1,67 +1,47 @@
-import Router from '@timo/common/components/Router';
-import UserContextProvider from '@timo/common/context/UserContextProvider';
import Container from '@timo/common/components/Container';
-import Title from '@timo/common/components/Title';
-import TopBarWithUser from '@timo/common/contextualComponents/TopBarWithUser';
-import ProtectedRoute from '@timo/common/contextualComponents/ProtectedRoute';
import Login from './routes/Login/Login';
import Entries from './routes/Entries/Entries';
import NewEntry from './routes/NewEntry/NewEntry';
import Profile from './routes/Profile/Profile';
-
-const routes = [
- { path: '/', name: 'Entries' },
- { path: '/login', name: 'Login' },
- { path: '/register', name: 'Register' },
- { path: '/new', name: 'NewEntry' },
- { path: '/profile', name: 'Profile' }
-];
+import TopBarWithUser from './contextualComponents/TopBarWithUser';
+import MachineContextProvider from './context/MachineContext';
+import Router from './components/Router';
const App = () => (
-
-
- {(routeName, history) => {
+
+
+ {(routeName) => {
let pageComponent = null;
switch (routeName) {
- case 'Login':
- pageComponent = ;
+ case 'login':
+ pageComponent = ;
break;
- case 'NewEntry':
+ case 'newEntry':
pageComponent = (
-
-
-
+
);
break;
- case 'Entries':
+ case 'entries':
pageComponent = (
-
-
-
+
);
break;
- case 'Profile':
+ case 'profile':
pageComponent = (
-
-
-
+
);
break;
- default:
- pageComponent = (
- Page not found
- );
}
return (
-
+
{pageComponent}
);
}}
-
+
);
export default App;
diff --git a/packages/xstate/src/components/Router.jsx b/packages/xstate/src/components/Router.jsx
new file mode 100644
index 0000000..08eae91
--- /dev/null
+++ b/packages/xstate/src/components/Router.jsx
@@ -0,0 +1,17 @@
+import PropTypes from 'prop-types';
+import useRootMachineState from '../hooks/useRootMachineState';
+
+const Router = ({ children }) => {
+ const state = useRootMachineState(state => state.value);
+ const route = state?.authenticated || state?.unauthenticated;
+ if (route && route !== 'unknown') {
+ return children(route);
+ }
+ return null;
+};
+
+Router.propTypes = {
+ children: PropTypes.func.isRequired
+};
+
+export default Router;
\ No newline at end of file
diff --git a/packages/xstate/src/components/Timer.jsx b/packages/xstate/src/components/Timer.jsx
new file mode 100644
index 0000000..d7e2d22
--- /dev/null
+++ b/packages/xstate/src/components/Timer.jsx
@@ -0,0 +1,17 @@
+import styles from '@timo/common/components/Timer/Timer.module.css';
+import useChildMachineState from '../hooks/useChildMachineState';
+
+const Timer = () => {
+ const timerValue = useChildMachineState('newEntry', state => state.context.timerValue);
+
+ // Format duration to HH:MM:SS
+ const formattedValue = new Date(timerValue * 1000).toISOString().slice(11, 19);
+
+ return (
+
+ {formattedValue}
+
+ );
+};
+
+export default Timer;
\ No newline at end of file
diff --git a/packages/xstate/src/context/MachineContext.jsx b/packages/xstate/src/context/MachineContext.jsx
new file mode 100644
index 0000000..b25fbbd
--- /dev/null
+++ b/packages/xstate/src/context/MachineContext.jsx
@@ -0,0 +1,23 @@
+import PropTypes from 'prop-types';
+import { createActor } from 'xstate';
+import { createContext } from 'react';
+import root from '../machines/root';
+
+const rootActor = createActor(root, { systemId: 'root' });
+rootActor.start();
+
+export const MachineContext = createContext();
+
+const MachineContextProvider = ({ children }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+MachineContextProvider.propTypes = {
+ children: PropTypes.node.isRequired
+};
+
+export default MachineContextProvider;
\ No newline at end of file
diff --git a/packages/xstate/src/contextualComponents/TopBarWithUser.jsx b/packages/xstate/src/contextualComponents/TopBarWithUser.jsx
new file mode 100644
index 0000000..d1cb48a
--- /dev/null
+++ b/packages/xstate/src/contextualComponents/TopBarWithUser.jsx
@@ -0,0 +1,21 @@
+import TopBar from '@timo/common/components/TopBar';
+import useRootMachine from '../hooks/useRootMachine';
+import useRootMachineState from '../hooks/useRootMachineState';
+
+const TopBarWithUser = () => {
+ const rootMachine = useRootMachine();
+ const userData = useRootMachineState(state => state.context.userData);
+
+ return (
+ rootMachine.send({ type: 'pushRoute', route: 'entries' })}
+ onAvatarClick={() => rootMachine.send({ type: 'pushRoute', route: 'profile' })}
+ />
+ );
+};
+
+export default TopBarWithUser;
diff --git a/packages/xstate/src/hooks/useChildMachine.js b/packages/xstate/src/hooks/useChildMachine.js
new file mode 100644
index 0000000..59b0f65
--- /dev/null
+++ b/packages/xstate/src/hooks/useChildMachine.js
@@ -0,0 +1,9 @@
+import { useSelector } from '@xstate/react';
+import useRootMachine from './useRootMachine';
+
+const useChildMachine = (childId) => {
+ const rootMachine = useRootMachine();
+ return useSelector(rootMachine, (state) => state.children[childId]);
+};
+
+export default useChildMachine;
\ No newline at end of file
diff --git a/packages/xstate/src/hooks/useChildMachineState.js b/packages/xstate/src/hooks/useChildMachineState.js
new file mode 100644
index 0000000..782dd21
--- /dev/null
+++ b/packages/xstate/src/hooks/useChildMachineState.js
@@ -0,0 +1,9 @@
+import { useSelector } from '@xstate/react';
+import useChildMachine from './useChildMachine';
+
+const useChildMachineState = (childId, selector) => {
+ const machine = useChildMachine(childId);
+ return useSelector(machine, selector);
+};
+
+export default useChildMachineState;
\ No newline at end of file
diff --git a/packages/xstate/src/hooks/useRootMachine.js b/packages/xstate/src/hooks/useRootMachine.js
new file mode 100644
index 0000000..082c661
--- /dev/null
+++ b/packages/xstate/src/hooks/useRootMachine.js
@@ -0,0 +1,8 @@
+import { useContext } from 'react';
+import { MachineContext } from '../context/MachineContext';
+
+const useRootMachine = () => {
+ return useContext(MachineContext);
+};
+
+export default useRootMachine;
\ No newline at end of file
diff --git a/packages/xstate/src/hooks/useRootMachineState.js b/packages/xstate/src/hooks/useRootMachineState.js
new file mode 100644
index 0000000..2db84bf
--- /dev/null
+++ b/packages/xstate/src/hooks/useRootMachineState.js
@@ -0,0 +1,9 @@
+import { useSelector } from '@xstate/react';
+import useRootMachine from './useRootMachine';
+
+const useRootMachineState = (selector) => {
+ const rootMachine = useRootMachine();
+ return useSelector(rootMachine, selector);
+};
+
+export default useRootMachineState;
\ No newline at end of file
diff --git a/packages/xstate/src/machines/changePassword.js b/packages/xstate/src/machines/changePassword.js
new file mode 100644
index 0000000..90bda1c
--- /dev/null
+++ b/packages/xstate/src/machines/changePassword.js
@@ -0,0 +1,55 @@
+import { setup, assign, fromPromise } from 'xstate';
+import { updatePassword } from '@timo/common/api';
+
+const changePasswordMachine = setup({
+ actors: {
+ updatePassword: fromPromise(async ({ input }) => updatePassword(input))
+ }
+}).createMachine({
+ id: 'changePassword',
+ initial: 'idle',
+ context: ({ input }) => ({
+ username: input.username,
+ statusMessage: null
+ }),
+ states: {
+ 'idle': {
+ on: {
+ 'save': {
+ target: 'saving'
+ }
+ }
+ },
+ 'saving': {
+ entry: [
+ assign({
+ statusMessage: 'Saving...'
+ })
+ ],
+ invoke: [
+ {
+ src: 'updatePassword',
+ input: ({ context, event }) => ({
+ username: context.username,
+ password: event.password,
+ newPassword: event.newPassword
+ }),
+ onDone: {
+ target: 'idle',
+ actions: assign({
+ statusMessage: 'Password updated'
+ })
+ },
+ onError: {
+ target: 'idle',
+ actions: assign({
+ statusMessage: ({ event }) => event.error.message
+ })
+ }
+ }
+ ]
+ }
+ }
+});
+
+export default changePasswordMachine;
\ No newline at end of file
diff --git a/packages/xstate/src/machines/customizeUser.js b/packages/xstate/src/machines/customizeUser.js
new file mode 100644
index 0000000..c090fa2
--- /dev/null
+++ b/packages/xstate/src/machines/customizeUser.js
@@ -0,0 +1,87 @@
+import { setup, assign, fromPromise, sendTo } from 'xstate';
+import { updateUser } from '@timo/common/api';
+
+const customizeUserMachine = setup({
+ actors: {
+ updateUser: fromPromise(async ({ input }) => updateUser(input))
+ }
+}).createMachine({
+ id: 'customizeUser',
+ initial: 'idle',
+ context: ({ input }) => ({
+ userId: input.userId,
+ username: input.username,
+ avatar_background: input.avatar_background,
+ avatar_character: input.avatar_character,
+ statusMessage: null
+ }),
+ states: {
+ 'idle': {
+ on: {
+ 'changeAvatarCharacter': {
+ actions: assign(({ event }) => ({
+ avatar_character: event.value
+ }))
+ },
+ 'changeAvatarBackground': {
+ actions: assign(({ event }) => ({
+ avatar_background: event.value
+ }))
+ },
+ 'changeUsername': {
+ actions: assign(({ event }) => ({
+ username: event.value
+ }))
+ },
+ 'save': {
+ target: 'saving'
+ }
+ }
+ },
+ 'saving': {
+ entry: [
+ assign({
+ statusMessage: 'Saving...'
+ })
+ ],
+ invoke: [
+ {
+ src: 'updateUser',
+ input: ({ context }) => ({
+ id: context.userId,
+ username: context.username,
+ avatar_character: context.avatar_character,
+ avatar_background: context.avatar_background
+ }),
+ onDone: {
+ target: 'idle',
+ actions: [
+ assign({
+ statusMessage: 'Profile updated'
+ }),
+ sendTo(
+ ({ system }) => system.get('root'),
+ ({ context }) => ({
+ type: 'updateUserData',
+ params: {
+ username: context.username,
+ avatar_character: context.avatar_character,
+ avatar_background: context.avatar_background
+ }
+ })
+ )
+ ]
+ },
+ onError: {
+ target: 'idle',
+ actions: assign({
+ statusMessage: ({ event }) => event.error.message
+ })
+ }
+ }
+ ]
+ }
+ }
+});
+
+export default customizeUserMachine;
\ No newline at end of file
diff --git a/packages/xstate/src/machines/entries.js b/packages/xstate/src/machines/entries.js
new file mode 100644
index 0000000..0a7bbef
--- /dev/null
+++ b/packages/xstate/src/machines/entries.js
@@ -0,0 +1,158 @@
+import { setup, fromPromise, assign } from 'xstate';
+import { listEntries, updateEntry, deleteEntry } from '@timo/common/api';
+import getDateString from '@timo/common/utils/getDateString';
+
+const now = new Date();
+const firstDateOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
+const lastDateOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
+
+const getTotalDuration = (entries) => {
+ return entries.reduce((total, entry) => {
+ const diff = new Date(entry.end_time) - new Date(entry.start_time);
+ return total + diff;
+ }, 0);
+};
+
+const getEntriesGroupedByDate = (entries) => {
+ const formatter = new Intl.DateTimeFormat('default', { dateStyle: 'medium' });
+ const groupedEntries = entries
+ .toReversed() // Entries are sorted by id which is ascending, so we need to reverse them
+ .reduce((grouped, entry) => {
+ const date = formatter.format(new Date(entry.start_time));
+ if (!grouped[date]) {
+ grouped[date] = [];
+ }
+ grouped[date].push(entry); // Order of object keys is not guaranteed but YOLO
+ return grouped;
+ }, {});
+ return Object.entries(groupedEntries)
+ .map(([date, dayEntries]) => [
+ date,
+ dayEntries.toReversed(), // Day entries are descending after being grouped, reverse them
+ getTotalDuration(dayEntries)
+ ]);
+};
+
+const entriesMachine = setup({
+ actors: {
+ getEntries: fromPromise(async ({ input }) => listEntries(input)),
+ updateEntry: fromPromise(async ({ input }) => updateEntry(input)),
+ deleteEntry: fromPromise(async ({ input }) => deleteEntry(input))
+ }
+}).createMachine({
+ id: 'entries',
+ initial: 'loading',
+ context: {
+ groupedEntries: [],
+ totalDuration: 0,
+ statusMessage: null,
+ filter: {
+ startDate: getDateString(firstDateOfMonth),
+ endDate: getDateString(lastDateOfMonth)
+ },
+ itemStatusMessage: {}
+ },
+ states: {
+ 'loading': {
+ invoke: {
+ src: 'getEntries',
+ input: ({ context }) => ({
+ from: context.filter.startDate,
+ to: context.filter.endDate
+ }),
+ onDone: {
+ target: 'idle',
+ actions: assign(({ event }) => ({
+ groupedEntries: getEntriesGroupedByDate(event.output),
+ totalDuration: getTotalDuration(event.output),
+ statusMessage: event.output.length === 0 ? 'No entries found' : null
+ }))
+ },
+ onError: {
+ target: 'idle',
+ actions: assign({
+ statusMessage: ({ event }) => event.error.message
+ })
+ }
+ }
+ },
+ 'idle': {
+ on: {
+ 'filter': {
+ target: 'loading',
+ actions: assign(({ event }) => ({
+ filter: {
+ startDate: event.startDate,
+ endDate: event.endDate
+ }
+ }))
+ },
+ 'refresh': {
+ target: 'loading'
+ },
+ 'updateEntry': {
+ target: 'updating-entry'
+ },
+ 'deleteEntry': {
+ target: 'deleting-entry'
+ }
+ }
+ },
+ 'updating-entry': {
+ entry: [
+ assign({
+ itemStatusMessage: ({ event }) => ({
+ [event.id]: 'Saving...'
+ })
+ })
+ ],
+ invoke: {
+ src: 'updateEntry',
+ input: ({ event }) => event.updatedEntry,
+ onDone: {
+ target: 'loading',
+ actions: assign({
+ itemStatusMessage: ({ event }) => ({
+ [event.updatedEntry.id]: null
+ })
+ })
+ },
+ onError: {
+ target: 'idle',
+ actions: assign({
+ statusMessage: ({ event }) => event.error.message
+ })
+ }
+ }
+ },
+ 'deleting-entry': {
+ entry: [
+ assign({
+ itemStatusMessage: ({ event }) => ({
+ [event.entryId]: 'Deleting...'
+ })
+ })
+ ],
+ invoke: {
+ src: 'deleteEntry',
+ input: ({ event }) => event.entryId,
+ onDone: {
+ target: 'loading',
+ actions: assign({
+ itemStatusMessage: ({ event }) => ({
+ [event.id]: null
+ })
+ })
+ },
+ onError: {
+ target: 'idle',
+ actions: assign({
+ statusMessage: ({ event }) => event.error.message
+ })
+ }
+ }
+ }
+ }
+});
+
+export default entriesMachine;
\ No newline at end of file
diff --git a/packages/xstate/src/machines/login.js b/packages/xstate/src/machines/login.js
new file mode 100644
index 0000000..c5d2708
--- /dev/null
+++ b/packages/xstate/src/machines/login.js
@@ -0,0 +1,73 @@
+import { setup, fromPromise, assign, sendTo } from 'xstate';
+import { login, register } from '@timo/common/api';
+
+const loginMachine = setup({
+ actors: {
+ login: fromPromise(async ({ input }) => login(input)),
+ register: fromPromise(async ({ input }) => register(input))
+ },
+ actions: {
+ authenticate: sendTo(
+ ({ system }) => system.get('root'),
+ ({ event }) => ({ type: 'authenticate', params: event.output })
+ )
+ }
+}).createMachine({
+ id: 'login',
+ context: {
+ statusMessage: null
+ },
+ initial: 'idle',
+ states: {
+ 'idle': {
+ on: {
+ 'login': {
+ target: 'logging-in',
+ actions: assign({
+ statusMessage: 'Logging in...'
+ })
+ },
+ 'register': {
+ target: 'registering',
+ actions: assign({
+ statusMessage: 'Registering...'
+ })
+ }
+ }
+ },
+ 'logging-in': {
+ invoke: {
+ src: 'login',
+ input: ({ event }) => ({ username: event.username, password: event.password }),
+ onDone: {
+ target: 'idle',
+ actions: { type: 'authenticate' }
+ },
+ onError: {
+ target: 'idle',
+ actions: assign({
+ statusMessage: ({ event }) => event.error.message
+ })
+ }
+ }
+ },
+ 'registering': {
+ invoke: {
+ src: 'register',
+ input: ({ event }) => ({ username: event.username, password: event.password }),
+ onDone: {
+ target: 'idle',
+ actions: { type: 'authenticate'}
+ },
+ onError: {
+ target: 'idle',
+ actions: assign({
+ statusMessage: ({ event }) => event.error.message
+ })
+ }
+ }
+ }
+ }
+});
+
+export default loginMachine;
\ No newline at end of file
diff --git a/packages/xstate/src/machines/newEntry.js b/packages/xstate/src/machines/newEntry.js
new file mode 100644
index 0000000..337a627
--- /dev/null
+++ b/packages/xstate/src/machines/newEntry.js
@@ -0,0 +1,130 @@
+import { setup, fromPromise, fromCallback, assign, sendTo } from 'xstate';
+import { createEntry } from '@timo/common/api';
+
+const newEntryMachine = setup({
+ actors: {
+ createEntry: fromPromise(async ({ input }) => createEntry(input)),
+ timer: fromCallback(({ sendBack, receive}) => {
+ const interval = setInterval(() => {
+ sendBack({
+ type: 'tick'
+ });
+ }, 1000);
+ receive((event) => {
+ if (event.type === 'stop') {
+ clearInterval(interval);
+ }
+ });
+ })
+ }
+}).createMachine({
+ id: 'newEntry',
+ initial: 'idle',
+ context: {
+ timerValue: 0,
+ statusMessage: null
+ },
+ states: {
+ 'idle': {
+ entry: [
+ assign({
+ timerValue: 0
+ })
+ ],
+ on: {
+ 'start': {
+ target: 'active'
+ }
+ }
+ },
+ 'active': {
+ entry: [
+ assign({
+ statusMessage: null
+ })
+ ],
+ invoke: {
+ src: 'timer',
+ id: 'timer'
+ },
+ on: {
+ 'tick': {
+ actions: assign({
+ timerValue: ({ context }) => context.timerValue + 1
+ })
+ },
+ 'pause': {
+ target: 'paused'
+ },
+ 'finish': {
+ target: 'finishing'
+ }
+ },
+ exit: [
+ sendTo('timer', {
+ type: 'stop'
+ })
+ ]
+ },
+ 'paused': {
+ on: {
+ 'resume': {
+ target: 'active'
+ },
+ 'finish': {
+ target: 'finishing'
+ }
+ }
+ },
+ 'finishing': {
+ entry: [
+ assign({
+ statusMessage: 'Saving...'
+ })
+ ],
+ invoke: {
+ src: 'createEntry',
+ input: ({ event, context }) => {
+ const endTimestamp = Math.floor(Date.now() / 1000);
+ return {
+ description: event.description,
+ start_time: `@${endTimestamp - context.timerValue}`,
+ end_time: `@${endTimestamp}`
+ };
+ },
+ onDone: {
+ target: 'finished'
+ },
+ onError: {
+ target: 'paused',
+ actions: assign({
+ statusMessage: ({ event }) => event.error.message
+ })
+ }
+ }
+ },
+ 'finished': {
+ entry: [
+ assign({
+ statusMessage: 'Time logged successfully'
+ }),
+ sendTo(
+ ({ system }) => system.get('entries'),
+ {
+ type: 'refresh'
+ }
+ )
+ ],
+ after: {
+ 2000: {
+ target: 'idle',
+ actions: assign({
+ statusMessage: null
+ })
+ }
+ }
+ }
+ }
+});
+
+export default newEntryMachine;
\ No newline at end of file
diff --git a/packages/xstate/src/machines/profile.js b/packages/xstate/src/machines/profile.js
new file mode 100644
index 0000000..0ab889c
--- /dev/null
+++ b/packages/xstate/src/machines/profile.js
@@ -0,0 +1,37 @@
+import { logout } from '@timo/common/api';
+import { fromPromise, sendTo, setup } from 'xstate';
+
+const profileMachine = setup({
+ actors: {
+ logout: fromPromise(logout)
+ }
+}).createMachine({
+ id: 'profile',
+ initial: 'idle',
+ states: {
+ 'idle': {
+ on: {
+ 'logout': {
+ target: 'logging-out'
+ }
+ }
+ },
+ 'logging-out': {
+ invoke: {
+ src: 'logout',
+ onDone: {
+ target: 'idle',
+ actions: sendTo(
+ ({ system }) => system.get('root'),
+ { type: 'unauthenticate' }
+ )
+ },
+ onError: {
+ target: 'idle'
+ }
+ }
+ }
+ }
+});
+
+export default profileMachine;
\ No newline at end of file
diff --git a/packages/xstate/src/machines/root.js b/packages/xstate/src/machines/root.js
new file mode 100644
index 0000000..e91831a
--- /dev/null
+++ b/packages/xstate/src/machines/root.js
@@ -0,0 +1,195 @@
+import { setup, spawnChild, sendTo, assign, fromPromise, fromCallback } from 'xstate';
+import history from 'history/browser';
+import { getUser } from '@timo/common/api';
+import customizeUserMachine from './customizeUser';
+import loginMachine from './login';
+import changePasswordMachine from './changePassword';
+import profileMachine from './profile';
+import entriesMachine from './entries';
+import newEntryMachine from './newEntry';
+
+const BASE_URL = import.meta.env.VITE_BASE_URL;
+
+const routes = {
+ login: `${BASE_URL}/login`,
+ entries: `${BASE_URL}/`,
+ newEntry: `${BASE_URL}/new`,
+ profile: `${BASE_URL}/profile`
+};
+
+const rootMachine = setup({
+ actors: {
+ getUser: fromPromise(getUser),
+ history: fromCallback(({ sendBack, receive }) => {
+ history.listen(({ location }) => {
+ sendBack({
+ type: 'locationChanged',
+ location
+ });
+ });
+ receive((event) => {
+ if (event.type === 'getLocation') {
+ sendBack({
+ type: 'locationChanged',
+ location: history.location
+ });
+ }
+ if (event.type === 'pushLocation') {
+ history.push(event.location);
+ }
+ if (event.type === 'replaceLocation') {
+ history.push(event.location);
+ }
+ });
+ }),
+ customizeUser: customizeUserMachine,
+ login: loginMachine,
+ changePassword: changePasswordMachine,
+ profile: profileMachine,
+ entries: entriesMachine,
+ newEntry: newEntryMachine
+ },
+ guards: {
+ isNewEntryRoute: ({ event }) => {
+ return event.location.pathname === routes.newEntry;
+ },
+ isProfileRoute: ({ event }) => {
+ return event.location.pathname === routes.profile;
+ }
+ }
+}).createMachine({
+ id: 'root',
+ entry: [
+ spawnChild('history', { id: 'history' })
+ ],
+ initial: 'unknown',
+ context: {
+ userData: null
+ },
+ states: {
+ 'unknown': {
+ invoke: {
+ src: 'getUser',
+ onDone: {
+ target: 'authenticated',
+ actions: assign({
+ userData: ({ event }) => ({
+ id: event.output.id,
+ username: event.output.username,
+ avatar_character: event.output.avatar_character,
+ avatar_background: event.output.avatar_background
+ })
+ })
+ },
+ onError: {
+ target: 'unauthenticated'
+ }
+ }
+ },
+ 'authenticated': {
+ invoke: {
+ // Invoke entries here so that it doesn't reload as we navigate across routes
+ src: 'entries',
+ id: 'entries',
+ systemId: 'entries'
+ },
+ initial: 'unknown',
+ states: {
+ 'unknown': {
+ entry: [
+ sendTo('history', { type: 'getLocation' })
+ ]
+ },
+ 'entries': {},
+ 'newEntry': {
+ invoke: {
+ src: 'newEntry',
+ id: 'newEntry'
+ }
+ },
+ 'profile': {
+ invoke: [
+ {
+ src: 'profile',
+ id: 'profile'
+ },
+ {
+ src: 'customizeUser',
+ id: 'customizeUser',
+ input: ({ context }) => ({
+ userId: context.userData.id,
+ username: context.userData.username,
+ avatar_character: context.userData.avatar_character,
+ avatar_background: context.userData.avatar_background
+ })
+ },
+ {
+ src: 'changePassword',
+ id: 'changePassword',
+ input: ({ context }) => ({
+ username: context.userData.username
+ })
+ }
+ ]
+ }
+ },
+ on: {
+ 'locationChanged': [
+ {
+ target: '.newEntry',
+ guard: 'isNewEntryRoute'
+ },
+ {
+ target: '.profile',
+ guard: 'isProfileRoute'
+ },
+ // Fallback to entries route if non specified or not found
+ {
+ target: '.entries'
+ }
+ ],
+ 'pushRoute': {
+ actions: sendTo('history', ({ event }) => ({
+ type: 'pushLocation',
+ location: routes[event.route]
+ }))
+ },
+ 'unauthenticate': {
+ target: 'unauthenticated'
+ }
+ }
+ },
+ 'unauthenticated': {
+ initial: 'login',
+ states: {
+ 'login': {
+ entry: [
+ sendTo('history', {
+ type: 'replaceLocation',
+ location: routes.login
+ })
+ ],
+ invoke: {
+ src: loginMachine,
+ id: 'login'
+ }
+ }
+ },
+ on: {
+ authenticate: {
+ target: 'authenticated',
+ actions: assign({
+ userData:({ event }) => ({
+ id: event.params.id,
+ username: event.params.username,
+ avatar_character: event.params.avatar_character,
+ avatar_background: event.params.avatar_background
+ })
+ })
+ }
+ }
+ }
+ }
+});
+
+export default rootMachine;
diff --git a/packages/xstate/src/routes/Entries/Entries.jsx b/packages/xstate/src/routes/Entries/Entries.jsx
index f4116d5..ef17b55 100644
--- a/packages/xstate/src/routes/Entries/Entries.jsx
+++ b/packages/xstate/src/routes/Entries/Entries.jsx
@@ -1,109 +1,57 @@
-import { useEffect, useState, createRef } from 'react';
-import PropTypes from 'prop-types';
-import { listEntries, updateEntry, deleteEntry } from '@timo/common/api';
import Entry from '@timo/common/components/Entry';
import Title from '@timo/common/components/Title';
import Input from '@timo/common/components/Input';
import Button from '@timo/common/components/Button';
import StatusMessage from '@timo/common/components/StatusMessage';
import formatDuration from '@timo/common/utils/formatDuration';
-import getDateString from '@timo/common/utils/getDateString';
import styles from './Entries.module.css';
import { ButtonVariants } from '@timo/common/components/Button/Button';
+import useChildMachineState from '../../hooks/useChildMachineState';
+import useChildMachine from '../../hooks/useChildMachine';
+import useRootMachine from '../../hooks/useRootMachine';
-const getTotalDuration = (entries) => {
- return entries.reduce((total, entry) => {
- const diff = new Date(entry.end_time) - new Date(entry.start_time);
- return total + diff;
- }, 0);
-};
-
-const getEntriesGroupedByDate = (entries) => {
- const formatter = new Intl.DateTimeFormat('default', { dateStyle: 'medium' });
- // Entries are sorted by id which is ascending, so we need to reverse them
- return entries.toReversed().reduce((grouped, entry) => {
- const date = formatter.format(new Date(entry.start_time));
- if (!grouped[date]) {
- grouped[date] = [];
- }
- grouped[date].push(entry);
- return grouped;
- }, {});
-};
-
-const now = new Date();
-const firstDateOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
-const lastDateOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
-
-const Entries = ({ history }) => {
- const [entries, setEntries] = useState(null);
- const [statusMessage, setStatusMessage] = useState(null);
- const [entryStatusMessage, setEntryStatusMessage] = useState({ id: null, message: null });
- const formRef = createRef(null);
+const Entries = () => {
+ const {
+ groupedEntries,
+ totalDuration,
+ statusMessage,
+ filter,
+ itemStatusMessage
+ } = useChildMachineState('entries', state => state.context);
+ const entriesMachine = useChildMachine('entries');
+ const rootMachine = useRootMachine();
const handleEdit = (updatedEntry) => {
- setEntryStatusMessage({ id: updatedEntry.id, message: 'Saving...' });
- updateEntry(updatedEntry).then(() => {
- setEntries(null);
- setEntryStatusMessage({ id: null, message: null });
- }).catch((error) => {
- setEntryStatusMessage({ id: updatedEntry.id, message: error.message });
+ entriesMachine.send({
+ type: 'updateEntry',
+ updatedEntry
});
};
const handleDelete = (entryId) => {
- setEntryStatusMessage({ id: entryId, message: 'Deleting...' });
- deleteEntry(entryId).then(() => {
- setEntries(null);
- setEntryStatusMessage({ id: null, message: null });
- }).catch((error) => {
- setEntryStatusMessage({ id: entryId, message: error.message });
+ entriesMachine.send({
+ type: 'deleteEntry',
+ entryId
});
};
- const handleListEntriesResponse = (entries) => {
- setEntries(entries);
- if (entries.length === 0) {
- setStatusMessage('No entries found');
- } else {
- setStatusMessage(null);
- }
- };
-
- const handleListEntriesError = (error) => {
- setStatusMessage(error.message);
- };
-
const handleFilter = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
- const from = formData.get('from');
- const to = formData.get('to');
- setStatusMessage('Loading...');
- listEntries({ from, to })
- .then(handleListEntriesResponse)
- .catch(handleListEntriesError);
+ entriesMachine.send({
+ type: 'filter',
+ startDate: formData.get('from'),
+ endDate: formData.get('to')
+ });
};
const handleNewClick = () => {
- history.push('./new');
+ rootMachine.send({
+ type: 'pushRoute',
+ route: 'newEntry'
+ });
};
- useEffect(() => {
- const formData = new FormData(formRef.current);
- const from = formData.get('from');
- const to = formData.get('to');
-
- if (entries === null) {
- setStatusMessage('Loading...');
- listEntries({
- from,
- to
- }).then(handleListEntriesResponse)
- .catch(handleListEntriesError);
- }
- }, [entries]);
-
return (
<>
Time entries
@@ -111,26 +59,25 @@ const Entries = ({ history }) => {
-
{statusMessage && }
- {entries?.length > 0 && (
+ {totalDuration > 0 && (
<>
Total
-
{formatDuration(getTotalDuration(entries))}
+
{formatDuration(totalDuration)}
- {Object.entries(getEntriesGroupedByDate(entries)).map(([date, dayEntries]) => (
+ {groupedEntries.map(([date, dayEntries, dayEntriesDuration]) => (
{date}
-
{formatDuration(getTotalDuration(dayEntries))}
+
{formatDuration(dayEntriesDuration)}
- {/* Entries are descending after being grouped, reverse them */}
- {dayEntries.toReversed().map((entry) => (
+ {dayEntries.map((entry) => (
{
end_time={entry.end_time}
onEdit={handleEdit}
onDelete={handleDelete}
- status={entryStatusMessage.id === entry.id ? entryStatusMessage.message : null}
+ status={itemStatusMessage[entry.id]}
/>
))}
@@ -151,8 +98,4 @@ const Entries = ({ history }) => {
);
};
-Entries.propTypes = {
- history: PropTypes.object.isRequired
-};
-
export default Entries;
diff --git a/packages/xstate/src/routes/Login/Login.jsx b/packages/xstate/src/routes/Login/Login.jsx
index 9113a93..bfdafd0 100644
--- a/packages/xstate/src/routes/Login/Login.jsx
+++ b/packages/xstate/src/routes/Login/Login.jsx
@@ -1,22 +1,15 @@
-import { useState, useEffect } from 'react';
-import PropTypes from 'prop-types';
-import { login, register } from '@timo/common/api';
-import useUser from '@timo/common/hooks/useUser';
import Input from '@timo/common/components/Input';
import Button, { ButtonVariants } from '@timo/common/components/Button';
import Title from '@timo/common/components/Title';
import StatusMessage from '@timo/common/components/StatusMessage';
-import styles from './Login.module.css';
+import useChildMachine from '../../hooks/useChildMachine';
+import useChildMachineState from '../../hooks/useChildMachineState';
-const Login = ({ history }) => {
- const user = useUser();
- const [statusMessage, setStatusMessage] = useState(null);
+import styles from './Login.module.css';
- useEffect(() => {
- if (user.status === 'authenticated') {
- history.replace('./');
- }
- }, [history, user]);
+const Login = () => {
+ const statusMessage = useChildMachineState('login', (state) => state.context.statusMessage);
+ const loginMachine = useChildMachine('login');
const handleFormSubmit = (e) => {
e.preventDefault();
@@ -26,23 +19,19 @@ const Login = ({ history }) => {
const username = formData.get('username');
const password = formData.get('password');
- setStatusMessage('Loading...');
-
if (action == 'login') {
- login({ username, password }).then((response) => {
- user.setAuthenticatedUser(response);
- history.replace('./');
- }).catch((error) => {
- setStatusMessage(error.message);
+ loginMachine.send({
+ type: 'login',
+ username,
+ password
});
}
if (action == 'register') {
- register({ username, password }).then((response) => {
- user.setAuthenticatedUser(response);
- history.replace('./');
- }).catch((error) => {
- setStatusMessage(error.message);
+ loginMachine.send({
+ type: 'register',
+ username,
+ password
});
}
};
@@ -63,10 +52,4 @@ const Login = ({ history }) => {
);
};
-Login.propTypes = {
- history: PropTypes.shape({
- replace: PropTypes.func.isRequired
- }).isRequired
-};
-
export default Login;
\ No newline at end of file
diff --git a/packages/xstate/src/routes/NewEntry/NewEntry.jsx b/packages/xstate/src/routes/NewEntry/NewEntry.jsx
index 27da0d6..548b2be 100644
--- a/packages/xstate/src/routes/NewEntry/NewEntry.jsx
+++ b/packages/xstate/src/routes/NewEntry/NewEntry.jsx
@@ -1,110 +1,76 @@
-import { useEffect, useState } from 'react';
-import { createEntry } from '@timo/common/api';
-import Timer from '@timo/common/components/Timer';
import Title from '@timo/common/components/Title';
import Input from '@timo/common/components/Input';
import Button, { ButtonVariants } from '@timo/common/components/Button';
import StatusMessage from '@timo/common/components/StatusMessage';
+import useChildMachine from '../../hooks/useChildMachine';
+import useChildMachineState from '../../hooks/useChildMachineState';
+import Timer from '../../components/Timer';
import styles from './NewEntry.module.css';
-const TimerState = {
- ACTIVE: 'active',
- PAUSED: 'paused',
- STOPPED: 'stopped'
-};
-
const NewEntry = () => {
- const [duration, setDuration] = useState(0);
- const [timerState, setTimerState] = useState(TimerState.STOPPED);
- const [description, setDescription] = useState('');
- const [statusMessage, setStatusMessage] = useState('');
-
- const handleStartClick = () => {
- if (description.length === 0) {
- setStatusMessage('Description is required');
- } else {
- setTimerState(TimerState.ACTIVE);
- setStatusMessage('');
- }
- };
-
- const handleStopClick = () => {
- setTimerState(TimerState.STOPPED);
- };
-
- const handlePauseClick = () => {
- setTimerState(TimerState.PAUSED);
- };
+ const timerState = useChildMachineState('newEntry', state => state.value);
+ const statusMessage = useChildMachineState('newEntry', state => state.context.statusMessage);
+ const newEntryMachine = useChildMachine('newEntry');
- const handleDescriptionChange = (e) => {
- setDescription(e.target.value);
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ const action = e.nativeEvent.submitter.value;
+ const formData = new FormData(e.target);
+ newEntryMachine.send({
+ type: action,
+ description: formData.get('description')
+ });
};
- useEffect(() => {
- if (timerState === TimerState.STOPPED && duration > 0) {
- setDuration(0);
- const endTimestamp = Math.floor(Date.now() / 1000);
- createEntry({
- description,
- start_time: `@${endTimestamp - duration}`,
- end_time: `@${endTimestamp}`
- }).then(() => {
- setStatusMessage('Time logged successfully');
- }).catch((error) => {
- setStatusMessage(error.message);
- });
- }
- });
-
return (
<>
New time entry
-
-
-
-
-
-
- {(timerState === TimerState.STOPPED && duration === 0) && (
-
+
+
>
);
};
diff --git a/packages/xstate/src/routes/NewEntry/NewEntry.module.css b/packages/xstate/src/routes/NewEntry/NewEntry.module.css
index 8a010a6..8083a78 100644
--- a/packages/xstate/src/routes/NewEntry/NewEntry.module.css
+++ b/packages/xstate/src/routes/NewEntry/NewEntry.module.css
@@ -21,6 +21,7 @@
justify-content: center;
align-items: center;
gap: 8px;
+ margin-top: 12px;
}
.entries {
diff --git a/packages/xstate/src/routes/Profile/Profile.jsx b/packages/xstate/src/routes/Profile/Profile.jsx
index 747d93e..d893967 100644
--- a/packages/xstate/src/routes/Profile/Profile.jsx
+++ b/packages/xstate/src/routes/Profile/Profile.jsx
@@ -1,18 +1,15 @@
import Title from '@timo/common/components/Title';
import Button, { ButtonVariants } from '@timo/common/components/Button';
-import useUser from '@timo/common/hooks/useUser';
-import { logout } from '@timo/common/api';
import styles from './Profile.module.css';
import ChangePassword from './sections/ChangePassword';
import CustomizeUser from './sections/CustomizeUser';
+import useChildMachine from '../../hooks/useChildMachine';
const Profile = () => {
- const user = useUser();
+ const profileMachine = useChildMachine('profile');
const handleLogoutClick = () => {
- logout().then(() => {
- user.clearUser();
- });
+ profileMachine.send({ type: 'logout' });
};
return (
diff --git a/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx b/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx
index 54b25aa..7d2684f 100644
--- a/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx
+++ b/packages/xstate/src/routes/Profile/sections/ChangePassword.jsx
@@ -1,32 +1,21 @@
-import { useState } from 'react';
import Input from '@timo/common/components/Input';
-import useUser from '@timo/common/hooks/useUser';
import StatusMessage from '@timo/common/components/StatusMessage';
import Button from '@timo/common/components/Button';
-import { updatePassword } from '@timo/common/api';
import styles from '../Profile.module.css';
+import useChildMachine from '../../../hooks/useChildMachine';
+import useChildMachineState from '../../../hooks/useChildMachineState';
const ChangePassword = () => {
- const user = useUser();
- const [passwordStatus, setPasswordStatus] = useState(null);
+ const { statusMessage } = useChildMachineState('changePassword', state => state.context);
+ const changePasswordMachine = useChildMachine('changePassword');
const handlePasswordFormSubmit = (e) => {
e.preventDefault();
-
const formData = new FormData(e.target);
- const password = formData.get('password');
- const newPassword = formData.get('newPassword');
-
- setPasswordStatus('Loading...');
-
- updatePassword({
- username: user?.data?.username,
- password,
- newPassword
- }).then(() => {
- setPasswordStatus('Password updated');
- }).catch((error) => {
- setPasswordStatus(error.message);
+ changePasswordMachine.send({
+ type: 'save',
+ password: formData.get('password'),
+ newPassword: formData.get('newPassword')
});
};
@@ -50,7 +39,7 @@ const ChangePassword = () => {
labelVisible
required
/>
- {passwordStatus && }
+ {statusMessage && }
diff --git a/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx b/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx
index 386e541..b4413e7 100644
--- a/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx
+++ b/packages/xstate/src/routes/Profile/sections/CustomizeUser.jsx
@@ -1,64 +1,44 @@
-import { useEffect, useState } from 'react';
import Avatar from '@timo/common/components/Avatar';
import RadioGroup from '@timo/common/components/RadioGroup';
import Input from '@timo/common/components/Input';
-import useUser from '@timo/common/hooks/useUser';
import StatusMessage from '@timo/common/components/StatusMessage';
import Button from '@timo/common/components/Button';
-import { updateUser } from '@timo/common/api';
+import useChildMachine from '../../../hooks/useChildMachine';
+import useChildMachineState from '../../../hooks/useChildMachineState';
import styles from '../Profile.module.css';
const CustomizeUser = () => {
- const user = useUser();
- const [customizeStatus, setCustomizeStatus] = useState(null);
- const [avatar, setAvatar] = useState({
- character: undefined,
- background: undefined
- });
-
- useEffect(() => {
- if (user?.data) {
- setAvatar({
- character: user.data.avatar_character,
- background: user.data.avatar_background
- });
- }
- }, [user]);
+ const customizeUserMachine = useChildMachine('customizeUser');
+ const {
+ username,
+ avatar_character,
+ avatar_background,
+ statusMessage
+ } = useChildMachineState('customizeUser', state => state.context);
const handleCustomizeFormSubmit = (e) => {
e.preventDefault();
- const formData = new FormData(e.target);
- const username = formData.get('username');
- const avatarCharacter = formData.get('avatar-character');
- const avatarBackground = formData.get('avatar-background');
-
- setCustomizeStatus('Loading...');
-
- updateUser({
- id: user?.data?.id,
- username,
- avatar_character: avatarCharacter,
- avatar_background: avatarBackground
- }).then(() => {
- setCustomizeStatus('Profile updated');
- // Clear the user in context and force refetch
- user.clearUser();
- }).catch((error) => {
- setCustomizeStatus(error.message);
- });
+ customizeUserMachine.send({ type: 'save' });
};
const handleAvatarBackgroundChange = (e) => {
- setAvatar({
- ...avatar,
- background: e.target.value
+ customizeUserMachine.send({
+ type: 'changeAvatarBackground',
+ value: e.target.value
});
};
const handleAvatarCharacterChange = (e) => {
- setAvatar({
- ...avatar,
- character: e.target.value
+ customizeUserMachine.send({
+ type: 'changeAvatarCharacter',
+ value: e.target.value
+ });
+ };
+
+ const handleUsernameChange = (e) => {
+ customizeUserMachine.send({
+ type: 'changeUsername',
+ value: e.target.value
});
};
@@ -66,8 +46,8 @@ const CustomizeUser = () => {
<>