npm package: @namphuongtechnologi/react
Description: Packages of react for namphuongtechnologies
Author: namphuongso
Latest version: 0.2.6

# @namphuongtechnologi/react

## Introduction

This project is designed to facilitate the reusability of React libraries. It includes a collection of reusable components, hooks, and logics. The main goal of this project is to streamline the development process by providing a set of pre-built elements that can be incorporated into various React applications. This not only saves time but also ensures consistency across different projects.

## Usage

### Table of Contents

- [Context](#context)
- [Hook-form](#hook-form)
- [Hooks](#hooks)
  - [UseBeforeLeave](#useBeforeLeave)
  - [UsePDF](#usePDF)
- [Router](#router)
- [Tanstack-Query](#tanstack-Query)
- [Permission](#permission)
- [Calendar Timeline](#calendar-timeline)

### Context

```tsx
import { Context } from '@namphuongtechnologi/react';

type ContextType = {
  value: string;
};

const exampleContext = new Context<ContextType>({
  name: 'example-context',
});

const App = () => {
  const ctxValue: ContextType = {
    value: 'Hello World',
  };

  return (
    <exampleContext.Provider value={ctxValue}>
      <Child />
    </exampleContext.Provider>
  );
};

const Child = () => {
  const context = exampleContext.useContext();

  console.log(context.value);

  return <>Child</>;
};
```

### Hook-form

```tsx
import { HookForm, InferForm } from '@namphuongtechnologi/react';
import { z } from 'zod';

const exampleForm = new HookForm({
  schema: z.object({
    className: z.string(),
    students: z.array(
      z.object({
        name: z.string(),
      })
    ),
  }),
});

type ExampleForm = InferForm<typeof exampleForm>;

const App = () => {
  const form = exampleForm.useForm({
    defaultValues: {
      className: 'Class A',
      students: [{ name: 'Student A' }, { name: 'Student B' }],
    },
  });

  const students = exampleForm.useFieldArray({
    control: form.control,
    name: 'students',
  });

  const handleSubmit = (data: ExampleForm) => {
    console.log(data);
  };

  return (
    <exampleForm.Provider {...form}>
      <form onSubmit={form.handleSubmit(handleSubmit)}>
        <exampleForm.Field name='className' render={({ field }) => <input {...field} />} />

        {students.fields.map((field, index) => (
          <exampleForm.Field
            key={field.__id}
            name={`students.${index}.name`}
            render={({ field: { onChange, value } }) => ({
              /* Render Content */
            })}
          />
        ))}
      </form>
    </exampleForm.Provider>
  );
};
```

### Hooks

#### useBeforeLeave

```tsx
import { useBeforeLeave } from '@namphuongtechnologi/react';
import { useRef, useState } from 'react';

const App = () => {
  const [canLeave, setCanLeave] = useState(false);

  const blocker = useBeforeLeave({
    when: canLeave,
    message: 'Are you sure you want to leave?',
    beforeUnload: true,
  });

  console.log(blocker);

  const toggleClick = () => setCanLeave((prev) => !prev);

  return (
    <>
      <button onClick={toggleClick}>{canLeave ? 'Disable' : 'Enable'} beforeunload</button>
      <button onClick={() => window.location.reload()}>Reload</button>
    </>
  );
};
```

#### usePDF

_PromiseDebounceFunction_

```tsx
import { usePDF } from '@namphuongtechnologi/react';
import { useRef, useState } from 'react';

type PDFValue = {
  value: string;
};

const App = () => {
  const ref = useRef<string[]>([]);

  const PDFClick = usePDF<PDFValue>(({ value }) => {
    const index = ref.current.indexOf(value);

    if (index === -1) {
      ref.current.push(value);
    } else {
      ref.current.splice(index, 1);
    }

    return {
      commit: () => {
        console.log(ref.current);

        ref.current = [];

        return Promise.resolve();
      },
      resolved: () => {},
      error: (error) => {
        console.log(error);
      },
    };
  }, 2000);

  return (
    <>
      {Array.from({ length: 10 }, (_, i) => (
        <Child key={i} no={i + 1} onClick={PDFClick} />
      ))}
    </>
  );
};

const Child = ({ no, onClick }: { no: number; onClick: ReturnType<typeof usePDF<PDFValue>> }) => {
  const [clicked, setClicked] = useState(false);

  const handleClick = async () => {
    setClicked(true);
    await onClick({ value: `${no}` });
    setClicked(false);
  };

  return (
    <button onClick={handleClick} disabled={clicked}>
      {clicked ? 'Loading' : `Button ${no}`}
    </button>
  );
};
```

### Router

```tsx
import { s, useSearchParams } from '@namphuongtechnologi/react';
import { z } from 'zod';

type Student = {
  id: number;
  name: string;
};

const App = () => {
  const [params, setParams] = useSearchParams({
    schema: z.object({
      keyword: s.string(),
      page: s.number().default(20),
      textArr: s.array<string>(['123']),
      numArr: s.array<number>([345]),
      dynamicArr: s.array<Student>(),
      active: s.boolean(),
    }),
  });

  const handleClick = () => {
    setParams({
      keyword: 'John Doe',
      page: 30,
      textArr: ['Marry', 'Jane', 'Wick'],
      numArr: [1, 2, 3],
      dynamicArr: [
        { id: 1, name: 'student_1' },
        { id: 2, name: 'student_2' },
      ],
      active: true,
    });
  };

  return (
    <>
      <pre style={{ textAlign: 'left' }}>{JSON.stringify(params, null, 2)}</pre>
      <button onClick={handleClick}>Set new params</button>
    </>
  );
};
```

### Tanstack-Query

```tsx
import { createQueryService, createInfiniteQueryService, createMutationService } from '@namphuongtechnologi/react';
import { QueryClient } from '@tanstack/react-query';
import { z } from 'zod';

import { axios } from './config/axios';

// Config
const queryClient = new QueryClient();

const QueryService = createQueryService(queryClient);
const InfiniteQueryService = createInfiniteQueryService(queryClient);
const MutationService = createMutationService(queryClient);

type InfiniteRecord<T> = {
  data: T[]
  totalRecord: number
}

declare module '@namphuongtechnologi/react' {
  interface Tanstack_InfiniteQuery_Service<T> extends InfiniteRecord<T> {}
}


// Define Type
interface IPost {
  id: number;
  title: string;
}

// API
const getPosts = async (payload?: { signal?: AbortSignal }) => {
  const { signal } = payload || {};

  // baseURL: 'https://jsonplaceholder.typicode.com'
  const { data } = await axios.instance.get<IPost[]>('/posts', { signal });

  return data;
};

const getPost = async (postId: PostSchema['postId'], payload?: { signal?: AbortSignal }) => {
  const { signal } = payload || {};

  const { data } = await axios.instance.get<IPost>(`/posts/${postId}`, { signal });

  return data;
};

const createPost = async (payload: Omit<IPost, 'id'>) => {
 await axios.instance.post<IPost>('/posts', payload);
};

// Define Schema
const postSchema = z.object({
  postId: z.number(),
});

type PostSchema = z.infer<typeof postSchema>;

// Define Query-service
const postsQuery = new QueryService({
  name: 'posts',
  fn: ({ signal }) => getPosts({ signal }),
});

const postQuery = new QueryService({
  name: 'post',
  schema: postSchema
  fn: ({ signal, payload: { postId } }) => getPost(postId, { signal }),
});

const CreatePost = new MutationService({
  name: 'create-post',
  fn: ({ payload }) => createPost(payload),
  onSuccess: () => {
    postsQuery.invalidate();
  },
});

// App
const App = () => {
  const createPost = CreatePost.useMutation();
  const { data, isLoading, isError } = postsQuery.useQuery();

  const handleCreatePost = () => {
    createPost.mutate(
      {
        title: 'New Post',
      },
      {
        onSuccess: () => alert('Success!'),
      }
    );
  };

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error</div>;
  if (!data) return <div>No Data</div>;

  return (
    <>
      <ul>
        {data.map((post) => (
          <li key={post.id} onClick={() => postQuery.ensureQueryData({ postId: post.id })}>
            {post.title}
          </li>
        ))}
      </ul>

      <button disabled={createPost.isPending} onClick={handleCreatePost}>
        Create Post
      </button>
    </>
  );
};

```

### Permission

```tsx
// config.ts
export enum User_Permission_Code {
  PERMISSION_A = 'permission-a',
  PERMISSION_B = 'permission-b',
}

declare module '@namphuongtechnologi/react' {
  interface IUser_Permission {
    code: User_Permission_Code;
    c: boolean;
    r: boolean;
    u: boolean;
    d: boolean;
  }
}

// PermissionProvider.tsx
import {
  createRecordPermission,
  PermissionProvider as NPPermissionProvider,
} from '@namphuongtechnologi/react';
import type { PropsWithChildren } from 'react';

export default function PermissionProvider({ children }: PropsWithChildren) {
  const permission = createRecordPermission('<permissions>', '<key>')

  return (
    <NPPermissionProvider permission={permission} notFoundPath='/not-found' notFoundComponent={<h1>404</h1>}>
      {children}
    </NPPermissionProvider>
  );
}

// App.tsx
import PermissionProvider from './PermissionProvider';

const App = () => {
  return <PermissionProvider>{/* Your App */}</PermissionProvider>;
};

// ExampleRoutes.tsx
import { Permission } from '@namphuongtechnologi/react';
import { Navigate, Route, Routes } from 'react-router-dom';

import { User_Permission_Code } from './config';
import ExamplePage from './ExamplePage';

const ExampleRoutes = () => {
  return (
    <Routes>
      <Route
        path=''
        element={
          <Permission
            isRoute
            record={{
              [User_Permission_Code['PERMISSION_A']]: ({ r }) => r,
            }}
          >
            <ExamplePage />
          </Permission>
        }
      />
      <Route path='*' element={<Navigate to='/not-found' />} />
    </Routes>
  );
};

// ExamplePage.tsx
import { useHavePermission } from '@namphuongtechnologi/react';

import { User_Permission_Code } from './config';

const ExamplePage = () => {
  const havePermission = useHavePermission();

  const isHaveCreatePermissionA = havePermission({
    [User_Permission_Code['PERMISSION_A']]: ({ c }) => c,
  });

  return (
    <div>
      ExamplePage
      {isHaveCreatePermissionA && (
        <span className='text-red-500'>If you see this, you are have create permission A</span>
      )}
    </div>
  );
};
```

### Calendar Timeline

```tsx
import {
  headerLabelFormat,
  primaryHeaderLabelFormat,
  Timeline,
  timelineBoundsChange,
} from '@namphuongtechnologi/react';
import type { RenderGroupProps, RenderItemProps } from '@namphuongtechnologi/react';
import { DB_DATE_TIME_FORMAT } from '@namphuongtechnologi/utils';
import dayjs from 'dayjs';
import type { TimelineGroupBase, TimelineItemBase } from 'react-calendar-timeline';
import { DateHeader, SidebarHeader, TimelineHeaders } from 'react-calendar-timeline';

import '@namphuongtechnologi/react/style.css';

interface Item {
  id: string;
  name: string;
  start: string;
  end: string;
}

interface Data {
  id: string;
  name: string;
  items: Item[];
}

type ItemType = TimelineItemBase<dayjs.Dayjs> & { data: Item };
type GroupType = TimelineGroupBase & { data: Omit<Data, 'items'> };

const defaultTimeStart = dayjs().startOf('month').toDate();
const defaultTimeEnd = dayjs().endOf('month').toDate();

const data: Data[] = Array.from({ length: 10 }).map((_, i) => ({
  id: `${i + 1}`,
  name: `user${i + 1}`,
  items: Array.from({ length: 5 }).map((_, j) => {
    const start = ~~(Math.random() * 25);
    const extra = 1 + ~~(Math.random() * 3);

    return {
      id: `${++id}`,
      name: `Code${i + j + 1}`,
      start: dayjs().startOf('M').add(start, 'day').format(DB_DATE_TIME_FORMAT),
      end: dayjs()
        .startOf('M')
        .add(start + extra, 'day')
        .format(DB_DATE_TIME_FORMAT),
    };
  }),
}));

const App = () => {
  const { groups, items } = data.reduce<{
    items: ItemType[];
    groups: GroupType[];
  }>(
    (acc, { items, ...cur }) => {
      acc.groups.push({
        id: cur.id,
        title: cur.name,
        data: cur,
      });

      items.forEach((item) => {
        acc.items.push({
          id: item.id,
          group: cur.id,
          title: item.name,
          start_time: dayjs(item.start),
          end_time: dayjs(item.end),
          data: item,
        });
      });

      return acc;
    },
    { items: [], groups: [] }
  );

  const onBoundsChange = (start: number, end: number) => {
    const value = timelineBoundsChange(start, end);

    if (!value) {
      return;
    }

    const { month, year } = value;

    console.log(month, year);
  };

  return (
    <Timeline>
      <Timeline.Calendar<ItemType, GroupType['data']>
        groups={groups}
        items={items}
        renderItem={Item}
        renderGroup={Group}
        fallback={<>Không có dữ liệu</>}
        defaultTimeStart={defaultTimeStart}
        defaultTimeEnd={defaultTimeEnd}
        onBoundsChange={onBoundsChange}
      >
        <TimelineHeaders>
          <SidebarHeader>
            {({ getRootProps }) => (
              <Timeline.SidebarHeader {...getRootProps()}>Tiêu đề</Timeline.SidebarHeader>
            )}
          </SidebarHeader>

          <DateHeader labelFormat={primaryHeaderLabelFormat} unit='primaryHeader' />

          <DateHeader labelFormat={headerLabelFormat} />
        </TimelineHeaders>

        <Timeline.Markers showNow />
      </Timeline.Calendar>
    </Timeline>
  );
};

const Item = ({ getItemProps, item }: RenderItemProps<ItemType>) => {
  // @ts-expect-error comment for unused
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const { onMouseDown, onMouseUp, onTouchStart, onTouchEnd, className, title, ...itemProps } =
    getItemProps({});

  return <p {...itemProps}>{item.title}</p>;
};

const Group = ({ group }: RenderGroupProps<GroupType['data']>) => {
  return <>{group.title}</>;
};
```