Skip to content

Commit aeffaff

Browse files
committed
main 🧊 add new demos
1 parent ca81a08 commit aeffaff

23 files changed

Lines changed: 436 additions & 378 deletions

File tree

‎packages/core/src/hooks/useFileDialog/useFileDialog.demo.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,10 @@ const Demo = () => {
8989

9090
<div className='px-4 py-3'>
9191
<textarea
92-
className='text-foreground placeholder:text-muted-foreground min-h-[140px] w-full resize-none bg-transparent text-sm leading-relaxed outline-none'
92+
className='text-foreground placeholder:text-muted-foreground no-scrollbar min-h-[140px] w-full resize-none bg-transparent text-sm leading-relaxed outline-none'
9393
id='description'
9494
placeholder='Add your message...'
95+
rows={8}
9596
{...messageField.register()}
9697
/>
9798
</div>

‎packages/core/src/hooks/useOptimistic/useOptimistic.demo.tsx‎

Lines changed: 199 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,239 @@
11
import {
2+
useClickOutside,
23
useDebounceCallback,
34
useDisclosure,
45
useMutation,
56
useOptimistic,
67
useQuery
78
} from '@siberiacancode/reactuse';
8-
import { HeartIcon, XIcon } from 'lucide-react';
9-
import { createPortal } from 'react-dom';
9+
import { HeartIcon, MoreHorizontalIcon, PinIcon, Trash2Icon, Undo2Icon } from 'lucide-react';
10+
import { useState } from 'react';
1011

1112
import { cn } from '@/utils/lib';
1213

13-
const POST = {
14-
name: 'reactuse',
15-
handle: '@reactuse',
16-
time: '2h',
17-
text: 'Like this post to see optimistic updates in action — the UI reacts instantly, but every 5th request fails and rolls back.',
14+
interface Post {
15+
handle: string;
16+
id: string;
17+
liked: boolean;
18+
likes: number;
19+
logo: string;
20+
name: string;
21+
pinned: boolean;
22+
text: string;
23+
time: string;
24+
}
25+
26+
const REGULAR_POST: Post = {
27+
id: 'regular',
28+
name: 'React',
29+
handle: '@reactjs',
30+
logo: 'https://cdn.simpleicons.org/react',
31+
text: 'Try liking or deleting this post — changes appear instantly, then sync or roll back.',
32+
time: '5h',
33+
pinned: false,
1834
liked: false,
19-
likes: 12
35+
likes: 42
2036
};
2137

22-
type Post = typeof POST;
38+
let POSTS: Post[] = [
39+
{
40+
id: 'pinned',
41+
name: 'reactuse',
42+
handle: '@reactuse',
43+
logo: 'https://reactuse.org/logo.svg',
44+
text: 'Welcome to reactuse — a collection of essential React hooks. This post is pinned and cannot be deleted.',
45+
time: '2h',
46+
pinned: true,
47+
liked: false,
48+
likes: 128
49+
},
50+
{ ...REGULAR_POST }
51+
];
52+
53+
const fetchPosts = () =>
54+
new Promise<Post[]>((resolve) =>
55+
setTimeout(() => resolve(POSTS.map((post) => ({ ...post }))), 500)
56+
);
2357

24-
let attempt = 0;
58+
const likePost = (id: string, liked: boolean) =>
59+
new Promise<void>((resolve) =>
60+
setTimeout(() => {
61+
const post = POSTS.find((item) => item.id === id);
62+
if (post) {
63+
post.liked = liked;
64+
post.likes += liked ? 1 : -1;
65+
}
66+
resolve();
67+
}, 500)
68+
);
2569

26-
// eslint-disable-next-line e18e/prefer-timer-args
27-
const fetchPost = () => new Promise<Post>((resolve) => setTimeout(() => resolve({ ...POST }), 500));
70+
const deletePost = (id: string) =>
71+
new Promise<void>((resolve) =>
72+
setTimeout(() => {
73+
POSTS = POSTS.filter((post) => post.id !== id);
74+
resolve();
75+
}, 800)
76+
);
2877

29-
const toggleLike = (next: boolean) =>
30-
new Promise<Post>((resolve, reject) => {
78+
const restorePost = () =>
79+
new Promise<void>((resolve) =>
3180
setTimeout(() => {
32-
attempt += 1;
33-
if (attempt % 5 === 0) {
34-
reject(new Error('Network error'));
35-
return;
36-
}
37-
POST.liked = next;
38-
POST.likes = 12 + (next ? 1 : 0);
39-
resolve({ ...POST });
40-
}, 500);
41-
});
81+
POSTS = [...POSTS, { ...REGULAR_POST }];
82+
resolve();
83+
}, 800)
84+
);
4285

43-
const Demo = () => {
44-
const postQuery = useQuery(fetchPost, { enabled: false, placeholderData: POST });
45-
const toggleLikeMutation = useMutation(toggleLike);
86+
interface PostCardProps {
87+
post: Post;
88+
onDelete: (id: string) => void;
89+
onLike: (post: Post) => void;
90+
}
91+
92+
const PostCard = ({ post, onLike, onDelete }: PostCardProps) => {
93+
const menu = useDisclosure();
94+
const ref = useClickOutside<HTMLDivElement>(() => menu.close());
4695

47-
const data = postQuery.data!;
96+
return (
97+
<article className='border-border flex flex-col gap-2 border-b py-3 last:border-b-0'>
98+
<div className='flex items-center gap-2'>
99+
<div className='bg-muted flex size-9 shrink-0 items-center justify-center overflow-hidden rounded-full'>
100+
<img alt={post.name} className='size-5' src={post.logo} />
101+
</div>
102+
<span className='text-foreground text-sm font-semibold'>{post.name}</span>
103+
<span className='text-muted-foreground text-xs'>{post.handle}</span>
104+
<span className='text-muted-foreground text-xs'>· {post.time}</span>
105+
106+
{post.pinned && <PinIcon className='text-muted-foreground ml-auto size-4 fill-current' />}
107+
108+
{!post.pinned && (
109+
<div className='relative ml-auto'>
110+
<button
111+
aria-label='More'
112+
className='text-muted-foreground hover:text-foreground'
113+
data-size='icon-sm'
114+
data-variant='ghost'
115+
type='button'
116+
onClick={menu.toggle}
117+
>
118+
<MoreHorizontalIcon className='size-4' />
119+
</button>
120+
121+
{menu.opened && (
122+
<div
123+
ref={ref}
124+
className='absolute top-full right-0 mt-1'
125+
data-slot='dropdown-menu-content'
126+
>
127+
<div
128+
data-slot='dropdown-menu-item'
129+
data-variant='destructive'
130+
onClick={() => {
131+
menu.close();
132+
onDelete(post.id);
133+
}}
134+
>
135+
<Trash2Icon />
136+
Delete
137+
</div>
138+
</div>
139+
)}
140+
</div>
141+
)}
142+
</div>
143+
144+
<p className='text-foreground text-sm leading-relaxed'>{post.text}</p>
145+
146+
<button
147+
className={cn(
148+
'flex w-fit items-center gap-1.5 px-0! text-sm transition-colors',
149+
post.liked ? 'text-destructive' : 'text-muted-foreground hover:text-destructive'
150+
)}
151+
data-variant='unstyled'
152+
type='button'
153+
onClick={() => onLike(post)}
154+
>
155+
<HeartIcon className='size-4' fill={post.liked ? 'currentColor' : 'none'} />
156+
{post.likes}
157+
</button>
158+
</article>
159+
);
160+
};
48161

49-
const toast = useDisclosure();
50-
const [optimisticLiked, updateOptimistic, setOptimisticLiked] = useOptimistic(
51-
data.liked,
52-
(_, value: boolean) => value
162+
const Demo = () => {
163+
const postsQuery = useQuery(fetchPosts, { enabled: false, placeholderData: POSTS });
164+
const likeMutation = useMutation(({ id, liked }: { id: string; liked: boolean }) =>
165+
likePost(id, liked)
53166
);
167+
const deleteMutation = useMutation(deletePost);
168+
const restoreMutation = useMutation(restorePost);
169+
170+
const data = postsQuery.data!;
54171

55-
const commit = useDebounceCallback((next: boolean) => {
56-
if (next === data.liked) return;
172+
const [posts, updateOptimistic, setPosts] = useOptimistic(data, (_, value) => value);
57173

174+
const [deletedIds, setDeletedIds] = useState<string[]>([]);
175+
176+
const likeCommit = useDebounceCallback((id: string, liked: boolean, optimistic: Post[]) => {
58177
const promise = (async () => {
59-
try {
60-
await toggleLikeMutation.mutateAsync(next);
61-
await postQuery.refetch();
62-
} catch {
63-
toast.open();
64-
setTimeout(toast.close, 1500);
65-
throw new Error('Network error');
66-
}
178+
await likeMutation.mutateAsync({ id, liked });
179+
await postsQuery.fetch();
67180
})();
68-
69-
updateOptimistic(next, promise);
181+
updateOptimistic(optimistic, promise);
70182
}, 600);
71183

72-
const onLike = () => {
73-
const next = !optimisticLiked;
74-
setOptimisticLiked(next);
75-
commit(next);
184+
const onLike = (target: Post) => {
185+
const liked = !target.liked;
186+
const optimistic = posts.map((item) =>
187+
item.id === target.id ? { ...item, liked, likes: item.likes + (liked ? 1 : -1) } : item
188+
);
189+
setPosts(optimistic);
190+
likeCommit(target.id, liked, optimistic);
76191
};
77192

78-
const likeCount = data.likes + (optimisticLiked === data.liked ? 0 : optimisticLiked ? 1 : -1);
193+
const onDelete = (id: string) => {
194+
setDeletedIds((current) => [...current, id]);
79195

80-
return (
81-
<section className='relative flex w-full max-w-md flex-col py-4'>
82-
<article className='flex flex-col gap-3'>
83-
<div className='flex items-center gap-2'>
84-
<div className='bg-muted flex size-9 shrink-0 items-center justify-center overflow-hidden rounded-full'>
85-
<img alt='reactuse' className='size-6' src='https://reactuse.org/logo.svg' />
86-
</div>
87-
<span className='text-foreground text-sm font-semibold'>{data.name}</span>
88-
<span className='text-muted-foreground text-xs'>{data.handle}</span>
89-
<span className='text-muted-foreground text-xs'>· {data.time}</span>
90-
</div>
196+
const promise = (async () => {
197+
await deleteMutation.mutateAsync(id);
198+
await postsQuery.fetch();
199+
})();
200+
updateOptimistic(
201+
posts.filter((item) => item.id !== id),
202+
promise
203+
);
204+
};
91205

92-
<p className='text-foreground text-sm leading-relaxed'>{data.text}</p>
206+
const onUndo = () => {
207+
setDeletedIds([]);
93208

94-
<div className='flex items-center'>
209+
const promise = (async () => {
210+
await restoreMutation.mutateAsync();
211+
await postsQuery.fetch();
212+
})();
213+
updateOptimistic([...posts, { ...REGULAR_POST }], promise);
214+
};
215+
216+
return (
217+
<section className='flex w-full max-w-md flex-col py-4'>
218+
{posts.map((item) => (
219+
<PostCard key={item.id} post={item} onDelete={onDelete} onLike={onLike} />
220+
))}
221+
222+
{!!deletedIds.length && (
223+
<div className='flex items-center justify-between pt-3'>
224+
<span className='text-muted-foreground text-sm'>Post deleted</span>
95225
<button
96-
className={cn(
97-
'flex items-center gap-1.5 px-0! text-sm transition-colors',
98-
optimisticLiked ? 'text-destructive' : 'text-muted-foreground hover:text-destructive'
99-
)}
100-
data-variant='unstyled'
226+
data-size='sm'
227+
data-variant='ghost'
228+
disabled={restoreMutation.isLoading}
101229
type='button'
102-
onClick={onLike}
230+
onClick={onUndo}
103231
>
104-
<HeartIcon className='size-4' fill={optimisticLiked ? 'currentColor' : 'none'} />
105-
{likeCount}
232+
<Undo2Icon className='size-3.5' />
233+
Undo
106234
</button>
107235
</div>
108-
</article>
109-
110-
{toast.opened &&
111-
createPortal(
112-
<div className='animate-in fade-in slide-in-from-bottom-4 fixed right-4 bottom-6 left-4 flex items-center gap-3 rounded-2xl border border-black/5 bg-white px-4 py-3.5 text-sm font-medium text-gray-900 shadow-xl duration-300 sm:right-6 sm:left-auto sm:w-auto sm:min-w-72 dark:border-white/10 dark:bg-neutral-900 dark:text-white'>
113-
<div className='bg-destructive flex size-6 shrink-0 items-center justify-center rounded-full'>
114-
<XIcon className='size-3.5 text-white' strokeWidth={3} />
115-
</div>
116-
Failed to change like
117-
</div>,
118-
document.body
119-
)}
236+
)}
120237
</section>
121238
);
122239
};

‎packages/core/src/hooks/useOrientation/useOrientation.demo.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const Demo = () => {
1818
</p>
1919
);
2020

21-
const portrait = orientation.value.orientationType;
21+
const portrait = orientation.value.orientationType?.startsWith('portrait') ?? true;
2222

2323
return (
2424
<section className='items-cesnter flex flex-col gap-5 p-6'>

‎packages/newdocs/app/(docs)/_components/layout/FunctionHeader/components/Burger/Burger.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export const Burger = ({ groups, className, ...props }: BurgerProps) => {
6262
<div className='flex flex-col gap-4'>
6363
<div className='text-muted-foreground text-sm font-medium'>Menu</div>
6464
<div className='flex flex-col gap-3'>
65-
<Link className='text-2xl font-medium' href='/' onClick={burger.toggle}>
65+
<Link className='text-2xl font-medium' href='/' onClick={burger.close}>
6666
Home
6767
</Link>
6868
</div>
@@ -72,7 +72,7 @@ export const Burger = ({ groups, className, ...props }: BurgerProps) => {
7272
<div className='text-muted-foreground text-sm font-medium'>{group.name}</div>
7373
<div className='flex flex-col gap-3'>
7474
{group.items.map((item) => (
75-
<Link key={item.url} href={item.url} onClick={burger.toggle}>
75+
<Link key={item.url} href={item.url} onClick={burger.close}>
7676
{item.name}
7777
</Link>
7878
))}

‎packages/newdocs/app/(docs)/_components/layout/FunctionSidebar/FunctionSidebar.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ export const FunctionSidebar = ({ groups, ...props }: FunctionSidebarProps) => {
3333
collapsible='none'
3434
{...props}
3535
>
36-
<div className='pointer-events-none absolute top-8 z-10 h-8 w-(--sidebar-menu-width) shrink-0 bg-linear-to-b from-background via-background/80 to-background/50 blur-xs' />
37-
<div className='from-background/75 via-background/25 pointer-events-none absolute inset-x-0 bottom-0 z-10 h-6 bg-gradient-to-t to-transparent' />
38-
<SidebarContent className='no-scrollbar h-full w-(--sidebar-menu-width) overflow-y-auto overscroll-contain px-2 pt-12 pb-22'>
36+
<div className='from-background via-background/80 to-background/50 pointer-events-none absolute inset-x-0 top-8 z-10 h-8 shrink-0 bg-linear-to-b blur-xs' />
37+
38+
<SidebarContent className='no-scrollbar mt-12 h-full w-(--sidebar-menu-width) overflow-y-auto overscroll-contain px-2 pb-22'>
3939
{groups.map((group, index) => (
4040
<SidebarGroup key={index}>
4141
<SidebarGroupLabel className='text-muted-foreground font-medium capitalize'>

‎packages/newdocs/app/(docs)/docs/[[...slug]]/page.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export const DocsPage = async (props: DocsPageProps) => {
7070

7171
return (
7272
<div
73-
className='flex scroll-mt-24 items-stretch pb-8 text-[1.05rem] sm:text-[15px] xl:w-full'
73+
className='flex scroll-mt-24 items-stretch pb-8 text-[1.05rem] sm:text-[15px] xl:w-full xl:gap-8 2xl:gap-10'
7474
data-slot='docs'
7575
>
7676
<div className='flex min-w-0 flex-1 flex-col'>
@@ -104,7 +104,7 @@ export const DocsPage = async (props: DocsPageProps) => {
104104
</div>
105105
</div>
106106
</div>
107-
<div className='sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[80svh] w-(--sidebar-width) flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex'>
107+
<div className='sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[80svh] w-(--sidebar-width) flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex xl:pl-2 2xl:pl-4'>
108108
{!!doc.toc.length && (
109109
<div className='no-scrollbar h-full overflow-y-auto overscroll-contain pt-12'>
110110
<DocsToc items={doc.toc} path={page.data.info.path} />

0 commit comments

Comments
 (0)