Stories Player
@reelkit/react-stories-player से बना, React के लिए Instagram जैसा stories प्लेयर overlay।
फ़ीचर
इंस्टॉलेशन
npm i @reelkit/react-stories-player @reelkit/react lucide-reactstyles import करना न भूलें:
import '@reelkit/react-stories-player/styles.css';आइकन
डिफ़ॉल्ट header आइकन के लिए lucide-react इस्तेमाल करता है। अगर आप कोई और आइकन लाइब्रेरी पसंद करते हैं, तो अपने देने के लिए renderHeader और renderNavigation इस्तेमाल करें।
क्विक स्टार्ट
StoriesOverlay कंपोनेंट फ़ुलस्क्रीन stories प्लेयर रेंडर करता है। Instagram जैसे entry points के लिए इसे StoriesRingList के साथ जोड़ें। StoriesGroup objects का array पास करें और isOpen से दिखना नियंत्रित करें।
import { useState } from 'react';
import {
StoriesOverlay,
StoriesRingList,
createStoriesViewedStateController,
type StoriesGroup,
} from '@reelkit/react-stories-player';
import '@reelkit/react-stories-player/styles.css';
const groups: StoriesGroup[] = [
{
author: {
id: 'user-1',
name: 'Alice',
avatar: '/cdn/samples/avatars/avatar-06.jpg',
verified: true,
},
stories: [
{
id: 's1-1',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-001.jpg',
},
{
id: 's1-2',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-002.jpg',
},
{
id: 's1-3',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-003.jpg',
},
],
},
{
author: {
id: 'user-2',
name: 'Bob',
avatar: '/cdn/samples/avatars/avatar-07.jpg',
},
stories: [
{
id: 's2-1',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-004.jpg',
},
{
id: 's2-2',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-005.jpg',
},
],
},
{
author: {
id: 'user-3',
name: 'Charlie',
avatar: '/cdn/samples/avatars/avatar-08.jpg',
verified: true,
},
stories: [
{
id: 's3-1',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-006.jpg',
},
],
},
];
export default function App() {
const [isOpen, setIsOpen] = useState(false);
const [selectedGroup, setSelectedGroup] = useState(0);
// What was seen: rings, resume and recording from one controller, kept in
// localStorage across reloads.
const [viewed] = useState(() =>
createStoriesViewedStateController({
storageKey: 'stories-seen',
groups: () => groups,
}),
);
const openStories = (groupIndex: number) => {
setSelectedGroup(groupIndex);
setIsOpen(true);
};
return (
<div style={{ padding: 16, background: '#0f172a', minHeight: '100vh' }}>
<StoriesRingList groups={groups} viewed={viewed} onSelect={openStories} />
<StoriesOverlay
isOpen={isOpen}
onClose={() => setIsOpen(false)}
groups={groups}
initialGroupIndex={selectedGroup}
viewed={viewed}
/>
</div>
);
}import { useState } from 'react';
import {
StoriesOverlay,
StoriesRingList,
createStoriesViewedStateController,
type StoriesGroup,
} from '@reelkit/react-stories-player';
import '@reelkit/react-stories-player/styles.css';
const groups: StoriesGroup[] = [
{
author: {
id: 'user-1',
name: 'Alice',
avatar: '/cdn/samples/avatars/avatar-06.jpg',
verified: true,
},
stories: [
{
id: 's1-1',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-001.jpg',
},
{
id: 's1-2',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-002.jpg',
},
{
id: 's1-3',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-003.jpg',
},
],
},
{
author: {
id: 'user-2',
name: 'Bob',
avatar: '/cdn/samples/avatars/avatar-07.jpg',
},
stories: [
{
id: 's2-1',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-004.jpg',
},
{
id: 's2-2',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-005.jpg',
},
],
},
{
author: {
id: 'user-3',
name: 'Charlie',
avatar: '/cdn/samples/avatars/avatar-08.jpg',
verified: true,
},
stories: [
{
id: 's3-1',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-006.jpg',
},
],
},
];
export default function App() {
const [isOpen, setIsOpen] = useState(false);
const [selectedGroup, setSelectedGroup] = useState(0);
// What was seen: rings, resume and recording from one controller, kept in
// localStorage across reloads.
const [viewed] = useState(() =>
createStoriesViewedStateController({
storageKey: 'stories-seen',
groups: () => groups,
}),
);
const openStories = (groupIndex: number) => {
setSelectedGroup(groupIndex);
setIsOpen(true);
};
return (
<div style={{ padding: 16, background: '#0f172a', minHeight: '100vh' }}>
<StoriesRingList groups={groups} viewed={viewed} onSelect={openStories} />
<StoriesOverlay
isOpen={isOpen}
onClose={() => setIsOpen(false)}
groups={groups}
initialGroupIndex={selectedGroup}
viewed={viewed}
/>
</div>
);
}ऐसी रिंग पंक्ति के लिए जो रीलो ड के बाद भी याद रखे कि दर्शक ने क्या देखा, स्टोरीज़ ट्रे रेसिपी देखें।
URL स्टेट
लाइव डेमो देखें →StoriesUrlOverlay एक अलग कंपोनेंट है जिसकी खुले होने की स्टेट address bar में रहती है। दोनों अक्ष एक ही parameter में चलते हैं — ?story=<group>.<story> — इसलिए चल रही story का ऐसा लिंक होता है जिसे शेयर, bookmark और back बटन से बंद किया जा सके। useOverlayUrlState और urlIndexTwoAxisKey से कंट्रोलर बनाएँ, फिर उसे controller के रूप में दें।
बिल्ट-इन keys
Stories दो-अक्ष वाली हैं, इसलिए कंट्रोलर में दो-अक्ष वाली key spread करें: urlIndexTwoAxisKey (ग्रुप और story स्थिति से) या urlStableIdTwoAxisKey (ग्रुप स्थिर id से) — दोनों @reelkit/react से भी export होती हैं। URL स्टेट गाइड और Core API देखें।
import {
StoriesUrlOverlay,
useOverlayUrlState,
urlIndexTwoAxisKey,
} from '@reelkit/react-stories-player';
import { Link } from 'react-router-dom';
const stories = useOverlayUrlState({
param: 'story',
...urlIndexTwoAxisKey({
outerCount: () => groups.length,
innerCounts: () => groups.map((g) => g.stories.length),
}),
});
// Opening a user is a link — the overlay reads the URL and opens itself.
{groups.map((g, i) => (
<Link key={g.author.id} to={`?story=${i}.0`}>{g.author.name}</Link>
))}
<StoriesUrlOverlay controller={stories} groups={groups} />- खोलने पर history में एक entry जुड़ती है। stories स्वाइप करना और यूज़र बदलना, दोनों उसे बदल देते हैं, इसलिए N नेविगेशन कोई entry नहीं जोड़ते और back का एक कदम हमेशा प्लेयर बंद कर देता है। Back बंद करता है; stories में पीछे नहीं जाता।
- अंदरूनी नेविगेशन भी साथ चलता है। Story index ग्रुप के स्तर पर जमा नहीं रहता — किसी यूज़र की stories में आगे बढ़ने पर
?story=2.n अपडेट होता है, इसलिए सीधा लिंक ठीक उसी story पर पहुँचता है। - Back सिर्फ़ तब बंद करता है जब प्लेयर ऐप के अंदर से खोला गया हो — लिंक ने entry जोड़ी हो। नए tab में सीधे खोले गए शेयर किए गए लिंक के पीछे कोई history नहीं होती, इसलिए ब्राउज़र का back साइट छोड़ देता है; ✕ बटन या Escape parameter को उसी जगह हटाकर पेज पर बना रहता है।
- किसी ग्रुप या story का नाम न लेने वाला parameter — पुराना bookmark, हाथ से बदली value, ग्रुप के अंत से आगे की story — प ड़ोसी खोलने के बजाय URL से हटा दिया जाता है।
Routing वाला ऐप — adapter पास करें। router के पीछे history.pushState लिखने से उसकी location पुरानी पड़ जाती है, और उसका अगला नेविगेशन parameter गिरा देता है:
import { useReactRouterUrlAdapter } from '@reelkit/react/react-router-url-adapter';
const adapter = useReactRouterUrlAdapter();
const stories = useOverlayUrlState({
param: 'story',
adapter,
...urlIndexTwoAxisKey({
outerCount: () => groups.length,
innerCounts: () => groups.map((g) => g.stories.length),
}),
});
<StoriesUrlOverlay controller={stories} groups={groups} />स्थिर लिंक। ग्रुप डिफ़ॉल्ट रूप से स्थिति पर आधारित है, इसलिए फ़ीड का क्रम बदलते ही bookmark किया गया ?story=2.0 कोई दूसरा यूज़र खोलता है। इसकी जगह ग्रुप को स्थिर id से पहचानें — outerCodec id को URL में लिखता है, outerLocator ढूँढता है कि वह कहाँ है। ग्रुप के अंदर story के लिए भी यही करने को innerCodec के साथ innerLocate और innerIdentify दें; इन्हें छोड़ दें तो story वाला हिस्सा साधारण स्थानीय index ही रहता है।
const stories = useOverlayUrlState({
param: 'story',
...urlIndexTwoAxisKey({
outerCount: () => groups.length,
innerCounts: () => groups.map((g) => g.stories.length),
// ?story=user_42.3
outerCodec: { decode: (raw) => raw, encode: (id) => id },
outerLocator: {
locate: (id) => groups.findIndex((g) => g.author.id === id),
identify: (index) => groups[index].author.id,
},
// Optional: ?story=user_42.story_7 — the story by id too
innerCodec: { decode: (raw) => raw, encode: (id) => id },
innerLocate: (outer, id) => {
const index = groups[outer].stories.findIndex((s) => s.id === id);
return index === -1 ? null : index;
},
innerIdentify: (outer, index) => groups[outer].stories[index].id,
}),
});अनंत फ़ीड। पेज लोड करना outerLocator का काम है, codec से स्वतंत्र। locate synchronous है, इसलिए यह सिर्फ़ लोड हो चुके ग्रुप के लिए जवाब देता है — जिस फ़ीड में 20 ग्रुप लोड हुए हैं, उसके ग्रुप 400 का शेयर किया गया लिंक खाली लौटता है। locateAsync विकल्प है, जो सिर्फ़ locate के न मिलने पर कॉल होता है; story को उसी ग्रुप की सीमा में दोबारा बाँधा जाता है जिस पर वह पहुँचता है।