Skip to content

Commit a49a275

Browse files
committed
main 🧊 new demos
1 parent 8fef43a commit a49a275

20 files changed

Lines changed: 1161 additions & 72 deletions

File tree

packages/core/src/bundle/hooks/useMemory/useMemory.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@ export const useMemory = () => {
3030
setValue(performance.memory);
3131
}, 1000);
3232
return () => clearInterval(intervalId);
33-
}, [supported]);
33+
}, []);
3434
return { supported, value };
3535
};

packages/core/src/hooks/useDebounceEffect/useDebounceEffect.demo.tsx

Lines changed: 125 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,140 @@
1-
import { useDebounceEffect, useField } from '@siberiacancode/reactuse';
1+
import {
2+
useAsync,
3+
useClickOutside,
4+
useDebounceEffect,
5+
useDisclosure
6+
} from '@siberiacancode/reactuse';
7+
import { CheckIcon, ChevronDownIcon, Loader2Icon, SearchIcon } from 'lucide-react';
28
import { useState } from 'react';
39

10+
import { cn } from '@/utils/lib';
11+
12+
interface Animal {
13+
emoji: string;
14+
name: string;
15+
}
16+
17+
const ANIMALS: Animal[] = [
18+
{ emoji: '🐶', name: 'Dog' },
19+
{ emoji: '🐱', name: 'Cat' },
20+
{ emoji: '🐸', name: 'Frog' },
21+
{ emoji: '🐙', name: 'Octopus' },
22+
{ emoji: '🦀', name: 'Crab' },
23+
{ emoji: '🐠', name: 'Fish' },
24+
{ emoji: '🐧', name: 'Penguin' },
25+
{ emoji: '🦅', name: 'Eagle' },
26+
{ emoji: '🦆', name: 'Duck' }
27+
];
28+
29+
const searchAnimals = (query: string): Promise<Animal[]> =>
30+
new Promise((resolve) => {
31+
setTimeout(
32+
() =>
33+
resolve(
34+
ANIMALS.filter((animal) => animal.name.toLowerCase().includes(query.toLowerCase()))
35+
),
36+
300
37+
);
38+
});
39+
440
const Demo = () => {
5-
const inputField = useField();
6-
const [list, setList] = useState<string[]>([]);
41+
const [search, setSearch] = useState('');
42+
const [debouncedQuery, setDebouncedQuery] = useState('');
43+
const [selected, setSelected] = useState<Animal | null>(null);
44+
45+
const dropdown = useDisclosure();
46+
const dropdownRef = useClickOutside<HTMLDivElement>(() => dropdown.close());
747

8-
const inputValue = inputField.watch();
48+
const animalsQuery = useAsync(() => searchAnimals(debouncedQuery), [debouncedQuery]);
949

1050
useDebounceEffect(
1151
() => {
12-
setList((currentList) => [...currentList, inputValue]);
52+
setDebouncedQuery(search);
1353
},
1454
500,
15-
[inputValue]
55+
[search]
1656
);
1757

58+
const onSelect = (animal: Animal) => {
59+
setSelected(animal);
60+
dropdown.close();
61+
setSearch('');
62+
setDebouncedQuery('');
63+
};
64+
65+
const results = animalsQuery.data ?? ANIMALS;
66+
const isLoading = animalsQuery.isLoading || search !== debouncedQuery;
67+
1868
return (
19-
<>
20-
<div>Write to see the debounce effect</div>
21-
<input {...inputField.register()} />
22-
<ul className='text-sm'>
23-
{list.map((item) => (
24-
<li key={item}>{item}</li>
25-
))}
26-
</ul>
27-
</>
69+
<section className='flex w-full max-w-sm flex-col gap-4 p-4'>
70+
<div className='flex flex-col gap-2'>
71+
<h3>Find your spirit animal</h3>
72+
<p className='text-muted-foreground text-sm'>
73+
The search waits <code>delay</code> after you stop typing before firing a request — no
74+
spam, no jank.
75+
</p>
76+
</div>
77+
78+
<div ref={dropdownRef} className='relative'>
79+
<div
80+
className='border-input bg-background flex h-10 w-full cursor-pointer items-center justify-between rounded-lg border px-3 text-sm transition-colors'
81+
onClick={() => dropdown.toggle()}
82+
>
83+
{selected && (
84+
<span className='flex items-center gap-2'>
85+
<span className='text-lg'>{selected.emoji}</span>
86+
<span>{selected.name}</span>
87+
</span>
88+
)}
89+
{!selected && <span className='text-muted-foreground'>Select an animal...</span>}
90+
<ChevronDownIcon
91+
className={cn('text-muted-foreground size-4', dropdown.opened && 'rotate-180')}
92+
/>
93+
</div>
94+
95+
{dropdown.opened && (
96+
<div className='bg-popover text-popover-foreground absolute top-full right-0 left-0 z-50 mt-2 overflow-hidden rounded-lg border shadow-md'>
97+
<div className='relative'>
98+
<SearchIcon className='text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2' />
99+
<input
100+
autoFocus
101+
className='h-10! w-full border-0! bg-inherit! pl-9! focus-visible:ring-0!'
102+
placeholder='Search animals...'
103+
type='text'
104+
value={search}
105+
onChange={(event) => setSearch(event.target.value)}
106+
/>
107+
{isLoading && (
108+
<Loader2Icon className='text-muted-foreground pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 animate-spin' />
109+
)}
110+
</div>
111+
112+
<div className='no-scrollbar max-h-60 overflow-y-auto p-1'>
113+
{!results.length && !isLoading && (
114+
<p className='text-muted-foreground py-6 text-center text-sm'>No animals found</p>
115+
)}
116+
117+
{results.map((animal) => {
118+
const isSelected = selected?.name === animal.name;
119+
return (
120+
<div
121+
key={animal.name}
122+
className='hover:bg-accent hover:text-accent-foreground flex cursor-pointer items-center justify-between gap-3 rounded-md px-2 py-1.5 text-sm'
123+
onClick={() => onSelect(animal)}
124+
>
125+
<span className='flex items-center gap-2'>
126+
<span className='text-lg'>{animal.emoji}</span>
127+
<span>{animal.name}</span>
128+
</span>
129+
{isSelected && <CheckIcon className='size-4' />}
130+
</div>
131+
);
132+
})}
133+
</div>
134+
</div>
135+
)}
136+
</div>
137+
</section>
28138
);
29139
};
30140

packages/core/src/hooks/useDebounceState/useDebounceState.demo.tsx

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,89 @@
11
import { useDebounceState } from '@siberiacancode/reactuse';
2+
import { useState } from 'react';
3+
4+
import { cn } from '@/utils/lib';
5+
6+
const DEFAULT_MARKDOWN = `# Welcome to reactuse\n\nA collection of **React hooks** for everyday tasks.\n\n## Quick start\n\nInstall via npm and you get **zero dependencies**, **TypeScript types** out of the box, and **fully tested** hooks ready to use.\n\nCheck it out at [reactuse.com](https://reactuse.com).`;
7+
8+
const formatInline = (text: string) =>
9+
text
10+
.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>')
11+
.replace(/\*(.+?)\*/g, '<i>$1</i>')
12+
.replace(/`(.+?)`/g, '<code>$1</code>')
13+
.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" target="_blank" rel="noreferrer">$1</a>');
14+
15+
const renderMarkdown = (markdown: string) => {
16+
const lines = markdown.split('\n');
17+
const html: string[] = [];
18+
19+
for (const line of lines) {
20+
const trimmed = line.trim();
21+
if (!trimmed) continue;
22+
23+
if (trimmed.startsWith('# ')) {
24+
html.push(`<h2>${trimmed.slice(2)}</h2>`);
25+
continue;
26+
}
27+
28+
if (trimmed.startsWith('## ')) {
29+
html.push(`<h3>${trimmed.slice(3)}</h3>`);
30+
continue;
31+
}
32+
33+
html.push(`<p>${formatInline(trimmed)}</p>`);
34+
}
35+
36+
return html.join('');
37+
};
238

339
const Demo = () => {
4-
const [debouncedValue, setDebouncedValue] = useDebounceState('', 500);
40+
const [markdown, setMarkdown] = useDebounceState(DEFAULT_MARKDOWN, 400);
41+
const [tab, setTab] = useState<'edit' | 'preview'>('edit');
542

643
return (
7-
<>
8-
<div>
9-
<label className='mb-2 block text-sm font-medium'>Enter value to see debounce effect</label>
10-
<input
11-
className='w-full'
12-
defaultValue={debouncedValue}
13-
placeholder='Enter value to see debounce effect'
14-
type='text'
15-
onChange={(event) => setDebouncedValue(event.target.value)}
16-
/>
17-
</div>
18-
<div className='flex flex-col gap-1'>
19-
<div className='text-sm'>
20-
Debounced value: <code>{debouncedValue || 'empty string'}</code>
44+
<section className='flex w-full max-w-3xl flex-col gap-4 p-4'>
45+
<div className='block md:hidden!' data-slot='tabs'>
46+
<div data-slot='tabs-list'>
47+
<button
48+
data-state={tab === 'edit' ? 'active' : 'inactive'}
49+
data-variant='tabs-trigger'
50+
type='button'
51+
onClick={() => setTab('edit')}
52+
>
53+
Edit
54+
</button>
55+
<button
56+
data-state={tab === 'preview' ? 'active' : 'inactive'}
57+
data-variant='tabs-trigger'
58+
type='button'
59+
onClick={() => setTab('preview')}
60+
>
61+
Preview
62+
</button>
2163
</div>
2264
</div>
23-
</>
65+
66+
<div className='grid grid-cols-1 gap-3 md:grid-cols-2'>
67+
<textarea
68+
className={cn(
69+
'no-scrollbar h-72! resize-none font-mono! text-xs!',
70+
tab === 'edit' ? 'block!' : 'hidden!',
71+
'md:block!'
72+
)}
73+
defaultValue={DEFAULT_MARKDOWN}
74+
onChange={(event) => setMarkdown(event.target.value)}
75+
/>
76+
77+
<div
78+
className={cn(
79+
'bg-muted/40 prose prose-xs dark:prose-invert no-scrollbar h-72 max-w-none overflow-y-auto rounded-lg p-4 text-xs',
80+
tab === 'preview' ? 'block!' : 'hidden!',
81+
'md:block!'
82+
)}
83+
dangerouslySetInnerHTML={{ __html: renderMarkdown(markdown) }}
84+
/>
85+
</div>
86+
</section>
2487
);
2588
};
2689

0 commit comments

Comments
 (0)