Skip to content

Commit

Permalink
move to reatom
Browse files Browse the repository at this point in the history
  • Loading branch information
osovv committed Jan 9, 2024
1 parent 17d70bf commit 3ae3c99
Show file tree
Hide file tree
Showing 25 changed files with 188 additions and 226 deletions.
1 change: 0 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ module.exports = configure({
presets.prettier(),
presets.typescript(),
presets.react({ newJSXTransform: true }),
presets.effector(),
],
extend: {
plugins: ['@reatom'],
Expand Down
3 changes: 0 additions & 3 deletions babel.config.cjs

This file was deleted.

2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@

Tasks are stored in IndexedDB.

This app is built with with [React](https://reactjs.org/) [TypeScript](https://www.typescriptlang.org/), [Vite](https://vitejs.dev/), [TailwindCSS](https://tailwindcss.com/), [Material Tailwind](https://www.material-tailwind.com/), [Effector](https://effector.dev), [Atomic Router](https://atomic-router.github.io/), [Storybook](https://storybook.js.org/) and [GitHub Actions](https://docs.github.com/en/actions). Deployed with [Vercel](https://vercel.app).
This app is built with with [React](https://reactjs.org/) [TypeScript](https://www.typescriptlang.org/), [Vite](https://vitejs.dev/), [TailwindCSS](https://tailwindcss.com/), [Material Tailwind](https://www.material-tailwind.com/), [Reatom](https://reatom.dev), [Atomic Router](https://atomic-router.github.io/), [Storybook](https://storybook.js.org/) and [GitHub Actions](https://docs.github.com/en/actions). Deployed with [Vercel](https://vercel.app).
4 changes: 2 additions & 2 deletions src/app/providers/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import compose from 'compose-function';
import { withEffectorScope } from './with-effector-scope';
import { withReatomContext } from './with-reatom-context';
import { withRouting } from './with-routing';
import { withTheme } from './with-theme';

export const withProviders = compose(withTheme, withRouting, withEffectorScope);
export const withProviders = compose(withTheme, withRouting, withReatomContext);
8 changes: 0 additions & 8 deletions src/app/providers/with-effector-scope.tsx

This file was deleted.

10 changes: 10 additions & 0 deletions src/app/providers/with-reatom-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createCtx } from '@reatom/core';
import { reatomContext } from '@reatom/npm-react';

const ctx = createCtx();

export const withReatomContext = (children: () => React.ReactNode) => () => {
return (
<reatomContext.Provider value={ctx}>{children()}</reatomContext.Provider>
);
};
90 changes: 48 additions & 42 deletions src/entities/task/model.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
import { combine, createEvent } from 'effector';
import { action, atom } from '@reatom/framework';
import { withLocalStorage } from '@reatom/persist-web-storage';
import { arrayMove } from '~/shared/lib/array';
import { createLocalStorageStore } from '~/shared/lib/effector-localstorage';
import { Id } from '~/shared/lib/id';
import { Optional } from '~/shared/lib/typescript';

type TaskStatus = 'active' | 'completed';

export interface Filter {
status: TaskStatus | undefined;
}

const DEFAULT_FILTER: Filter = {
status: undefined,
};

export const { $store: $filter, getLocalStorageValueFx: getFilterValueFx } =
createLocalStorageStore<Filter>('filter', DEFAULT_FILTER);

type TaskId = Id;

export interface Task {
Expand All @@ -32,22 +21,15 @@ export type TaskDataWithoutStatus = Omit<TaskData, 'status'>;

export type TaskDataOptional = Optional<TaskData>;

const updatedTask = (task: Task, updatedTaskData: TaskDataOptional): Task => ({
...task,
...updatedTaskData,
});

export const taskUpdated = createEvent<{
id: TaskId;
data: TaskDataOptional;
}>();
export interface Filter {
status: TaskStatus | undefined;
}

export const taskMoved = createEvent<{
from: number;
to: number;
}>();
const DEFAULT_FILTER: Filter = {
status: undefined,
};

const initialTasks: Array<Task> = [
const INITIAL_TASKS: Array<Task> = [
{
id: '1',
status: 'active',
Expand All @@ -61,26 +43,50 @@ const initialTasks: Array<Task> = [
},
];

export const { $store: $tasks, getLocalStorageValueFx: getTasksValueFx } =
createLocalStorageStore('tasks', initialTasks);
export const filterAtom = atom(DEFAULT_FILTER, 'filterAtom').pipe(
withLocalStorage('filter'),
);

export const $visibleTasks = combine($tasks, $filter, (tasks, filter) =>
tasks.filter(
(task) => filter.status === undefined || filter.status === task.status,
),
export const tasksAtom = atom(INITIAL_TASKS, 'tasksAtom').pipe(
withLocalStorage('tasks'),
);

$tasks.on(
taskUpdated,
(currentTasks, { id: updatedTaskId, data: updatedTaskData }) =>
currentTasks.map((task) => {
if (task.id === updatedTaskId) {
return updatedTask(task, updatedTaskData);
export const visibleTasksAtom = atom((ctx) => {
const tasks = ctx.spy(tasksAtom);

const filter = ctx.spy(filterAtom);

return tasks.filter(
(task) => filter.status === undefined || filter.status === task.status,
);
}, 'visibleTasksAtom');

const updatedTask = (task: Task, updatedTaskData: TaskDataOptional): Task => ({
...task,
...updatedTaskData,
});

export const updateTask = action(
(ctx, { id, data }: { id: TaskId; data: TaskDataOptional }) => {
const oldTasks = ctx.get(tasksAtom);

const newTasks = oldTasks.map((task) => {
if (task.id === id) {
return updatedTask(task, data);
}
return task;
}),
});

tasksAtom(ctx, newTasks);
},
'updateTask',
);

$tasks.on(taskMoved, (currentTasks, { from, to }) =>
arrayMove(currentTasks, { from, to }),
export const moveTask = action(
(ctx, { from, to }: { from: number; to: number }) => {
const oldTasks = ctx.get(tasksAtom);
const newTasks = arrayMove(oldTasks, { from, to });
tasksAtom(ctx, newTasks);
},
'moveTask',
);
34 changes: 15 additions & 19 deletions src/entities/task/ui/task-card/ui.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,27 @@
import { reatomContext, useCreateCtx } from '@reatom/npm-react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { fork } from 'effector';
import { Provider } from 'effector-react/scope';
import { $tasks } from '../../model';
import { taskModel } from '../..';
import { TaskCard } from './ui';

export default {
title: 'Entities/Task/TaskCard',
component: TaskCard,
decorators: [
(storyFn) => {
const scope = fork({
values: [
[
$tasks,
[
{
id: '1',
status: 'active',
title: 'Make a leather wallet',
description: 'Check YouTube for tutorial',
},
],
],
],
});
const ctx = useCreateCtx((ctx) => {});

return <Provider value={scope}>{storyFn()}</Provider>;
taskModel.tasksAtom(ctx, [
{
id: '1',
status: 'active',
title: 'Make a leather wallet',
description: 'Check YouTube for tutorial',
},
]);

return (
<reatomContext.Provider value={ctx}>{storyFn()}</reatomContext.Provider>
);
},
],
} as ComponentMeta<typeof TaskCard>;
Expand Down
16 changes: 8 additions & 8 deletions src/entities/task/ui/task-card/ui.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Typography } from '@material-tailwind/react';
import { useAtom } from '@reatom/npm-react';
import cn from 'classnames';
import { useStoreMap } from 'effector-react/scope';
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { getEntityById } from '~/shared/lib/effector';
import { getEntityById } from '~/shared/lib/entity';
import { mergeRefs } from '~/shared/lib/react';
import { $tasks, Task } from '../../model';
import { Task, tasksAtom } from '../../model';

export interface TaskCardProps {
id: Task['id'];
Expand All @@ -24,10 +24,10 @@ export const TaskCard = ({
onDelete,
onEdit,
}: TaskCardProps) => {
const task = useStoreMap({
store: $tasks,
keys: [id],
fn: (tasks, [taskId]) => getEntityById(tasks, taskId),
const [task] = useAtom((ctx) => {
const tasks = ctx.spy(tasksAtom);

return getEntityById(tasks, id);
});

const idStr = React.useMemo(() => `task-card-${id}`, [id]);
Expand Down Expand Up @@ -67,7 +67,7 @@ export const TaskCard = ({
{task.title}
</Typography>
<Typography variant='small' className='truncate text-gray-500'>
{task.description}
{task.description ?? ''}
</Typography>
<div className='absolute top-0 right-0 flex gap-1 bg-white opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-focus:bg-gray-100 '>
{EditSlot}
Expand Down
9 changes: 5 additions & 4 deletions src/entities/task/ui/task-editor/ui.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { reatomContext, useCreateCtx } from '@reatom/npm-react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { fork } from 'effector';
import { Provider } from 'effector-react/scope';
import { TaskEditor } from './ui';

export default {
Expand All @@ -18,9 +17,11 @@ export default {
},
decorators: [
(storyFn) => {
const scope = fork();
const ctx = useCreateCtx();

return <Provider value={scope}>{storyFn()}</Provider>;
return (
<reatomContext.Provider value={ctx}>{storyFn()}</reatomContext.Provider>
);
},
],
} as ComponentMeta<typeof TaskEditor>;
Expand Down
23 changes: 12 additions & 11 deletions src/features/add-task/model.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { createEvent } from 'effector';
import { action } from '@reatom/framework';
import { taskModel } from '~/entities/task';
import { TaskDataWithoutStatus } from '~/entities/task/model';
import { getId } from '~/shared/lib/id';

const taskCreated = createEvent<taskModel.Task>();

export const taskCreatedByUser =
taskCreated.prepend<taskModel.TaskDataWithoutStatus>((taskData) => ({
export const createTask = action((ctx, data: TaskDataWithoutStatus) => {
const newTask = {
id: getId(),
status: 'active',
...taskData,
}));
...data,
} as taskModel.Task;

const tasks = ctx.get(taskModel.tasksAtom);

const newTasks = [...tasks, newTask];

taskModel.$tasks.on(taskCreated, (currentTasks, newTask) => [
...currentTasks,
newTask,
]);
taskModel.tasksAtom(ctx, newTasks);
}, 'createTask');
6 changes: 3 additions & 3 deletions src/features/add-task/ui.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Button } from '@material-tailwind/react';
import { useUnit } from 'effector-react/scope';
import { useAction } from '@reatom/npm-react';
import { useCallback, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { TaskEditor, taskModel } from '~/entities/task';
import { taskCreatedByUser } from './model';
import { createTask } from './model';

export const AddTask = () => {
const [showForm, setShowForm] = useState(false);
Expand All @@ -13,7 +13,7 @@ export const AddTask = () => {

const submitButtonText = 'Add task';

const onSubmit = useUnit(taskCreatedByUser);
const onSubmit = useAction(createTask);

const handleSubmit = (payload: taskModel.TaskDataWithoutStatus) => {
setEditorKey((key) => key + 1);
Expand Down
12 changes: 6 additions & 6 deletions src/features/delete-task/model.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createEvent } from 'effector';
import { taskModel } from '~/entities/task';
import { action } from '@reatom/framework';
import { tasksAtom } from '~/entities/task/model';

export const taskRemoved = createEvent<taskModel.Task['id']>();
export const removeTask = action((ctx, id) => {
const tasks = ctx.get(tasksAtom);

taskModel.$tasks.on(taskRemoved, (currentTasks, removedTaskId) =>
currentTasks.filter((task) => task.id !== removedTaskId),
);
return tasks.filter((task) => task.id !== id);
}, 'removeTask');
18 changes: 9 additions & 9 deletions src/features/delete-task/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import {
DialogHeader,
IconButton,
} from '@material-tailwind/react';
import { useStoreMap, useUnit } from 'effector-react/scope';
import { useAction, useAtom } from '@reatom/npm-react';
import { useCallback, useState } from 'react';
import { taskModel } from '~/entities/task';
import { getEntityById } from '~/shared/lib/effector';
import { tasksAtom } from '~/entities/task/model';
import { getEntityById } from '~/shared/lib/entity';
import { Icon } from '~/shared/ui';
import { taskRemoved } from './model';
import { removeTask } from './model';

interface DeleteTaskProps {
id: taskModel.Task['id'];
Expand All @@ -28,10 +29,9 @@ const taskTitleString = (task: taskModel.Task | undefined): string => {
};

export const DeleteTask = ({ id }: DeleteTaskProps) => {
const task = useStoreMap({
store: taskModel.$tasks,
keys: [id],
fn: (tasks, [taskId]) => getEntityById(tasks, taskId),
const [task] = useAtom((ctx) => {
const tasks = ctx.spy(tasksAtom);
return getEntityById(tasks, id);
});

const [showConfirmation, setShowConfirmation] = useState(false);
Expand All @@ -41,10 +41,10 @@ export const DeleteTask = ({ id }: DeleteTaskProps) => {
[],
);

const removeTask = useUnit(taskRemoved);
const onRemove = useAction(removeTask);

const onSubmit = () => {
removeTask(id);
onRemove(id);
handleConfirmation();
};

Expand Down
Loading

0 comments on commit 3ae3c99

Please sign in to comment.