Skip to content

Commit cc8f2b9

Browse files
committed
example: React tutorial prepared to be demo app on website
fix demo app
1 parent a727693 commit cc8f2b9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+1985
-974
lines changed

README.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,39 +42,39 @@ Here are some of the features we support:
4242

4343
## React sample apps
4444

45-
### Simple React app with stories
45+
### React demo app with stories
4646

47-
Deployed version: https://feeds-react-tutorial-getstreamio.vercel.app/
47+
Deployed version: https://feeds-react-demo.vercel.app
4848

4949
Prerequisites:
5050

5151
- Install dependecies: `yarn`
5252
- Build React SDK: `yarn build:client` and `yarn build:react-sdk`
53-
- Create a `.env` file in `sample-apps/react-sample-app` with one of the following content:
53+
- Create a `.env` file in `sample-apps/react-demo` with one of the following content:
5454

5555
Use this if you want to use a single user only:
5656

5757
```
58-
VITE_API_KEY=your_api_key_here
59-
VITE_USER_ID=your_user_id_here
60-
VITE_USER_NAME=Your Name
61-
VITE_USER_TOKEN=your_user_token_here
58+
NEXT_PUBLIC_API_KEY=your_api_key_here
59+
NEXT_PUBLIC_USER_ID=your_user_id_here
60+
NEXT_PUBLIC_USER_NAME=Your Name
61+
NEXT_PUBLIC_USER_TOKEN=your_user_token_here
6262
```
6363

6464
If your app [is configured to accept development tokens](https://getstream.io/activity-feeds/docs/javascript/tokens-and-authentication/#developer-tokens), you can use dev tokens to test with muliple users, in this case just provide an API key:
6565

6666
```
67-
VITE_API_KEY=your_api_key_here
67+
NEXT_PUBLIC_API_KEY=your_api_key_here
6868
```
6969

7070
If you have a token provider backend, you can also provide a URL that takes `user_id` as a query param:
7171

7272
```
73-
VITE_API_KEY=your_api_key_here
74-
VITE_TOKEN_URL=optional,no need for user info in this case
73+
NEXT_PUBLIC_API_KEY=your_api_key_here
74+
NEXT_PUBLIC_TOKEN_URL=optional,no need for user info in this case
7575
```
7676

77-
After the above steps run the following command in `sample-apps/react-tutorial`:
77+
After the above steps run the following command in `sample-apps/react-demo`:
7878

7979
```
8080
yarn dev

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"build:client": "yarn workspace @stream-io/feeds-client run build",
1616
"build:react-native-sdk": "yarn workspace @stream-io/feeds-react-native-sdk run build",
1717
"build:react-sdk": "yarn workspace @stream-io/feeds-react-sdk run build",
18-
"build:react-tutorial": "yarn workspaces foreach -Avp --topological-dev --include '{packages/{feeds-client,react-sdk},sample-apps/react-tutorial}' run build",
18+
"build:react-demo": "yarn workspaces foreach -Avp --topological-dev --include '{packages/{feeds-client,react-sdk},sample-apps/react-demo}' run build",
1919
"clean:all": "yarn workspaces foreach -Avp run clean",
2020
"lint:all": "eslint --max-warnings=0 --cache '**/*.{ts,tsx}'",
2121
"lint:all:fix": "eslint --max-warnings=0 '**/*.{ts,tsx}' --fix",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
NEXT_PUBLIC_API_KEY=your_api_key_here
2+
NEXT_PUBLIC_USER_ID=your_user_id_here
3+
NEXT_PUBLIC_USER_NAME=Your Name
4+
NEXT_PUBLIC_USER_TOKEN=your_user_token_here
5+
NEXT_PUBLIC_TOKEN_URL=optional,no need for user info in this case

sample-apps/react-demo/.gitignore

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.*
7+
.yarn/*
8+
!.yarn/patches
9+
!.yarn/plugins
10+
!.yarn/releases
11+
!.yarn/versions
12+
13+
# testing
14+
/coverage
15+
16+
# next.js
17+
.next/
18+
out/
19+
20+
# production
21+
/build
22+
23+
# misc
24+
.DS_Store
25+
*.pem
26+
27+
# debug
28+
npm-debug.log*
29+
yarn-debug.log*
30+
yarn-error.log*
31+
32+
# env files (can opt-in for committing if needed)
33+
.env*
34+
35+
# vercel
36+
.vercel
37+
38+
# typescript
39+
*.tsbuildinfo
40+
next-env.d.ts
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
'use client';
2+
3+
import { useNotificationStatus } from '@stream-io/feeds-react-sdk';
4+
import { type PropsWithChildren } from 'react';
5+
import { FollowSuggestions } from './components/FollowSuggestions';
6+
import { useOwnFeedsContext } from './own-feeds-context';
7+
import { SearchInput } from './components/utility/SearchInput';
8+
import { NavLink } from './components/utility/NavLink';
9+
10+
export const AppSkeleton = ({ children }: PropsWithChildren) => {
11+
const { ownNotifications } = useOwnFeedsContext();
12+
const notificationStatus = useNotificationStatus(ownNotifications);
13+
const unreadCount = notificationStatus?.unread ?? 0;
14+
15+
return (
16+
<div className="drawer h-full max-h-full lg:drawer-open">
17+
<input id="my-drawer" type="checkbox" className="drawer-toggle" />
18+
<div className="drawer-content max-h-full min-h-0 h-full max-h-full flex flex-col gap-1 items-center justify-center">
19+
<nav className="hidden md:flex lg:hidden navbar w-full bg-base-100">
20+
<div className="flex-none lg:hidden">
21+
<label
22+
htmlFor="my-drawer"
23+
className="drawer-button btn btn-square btn-ghost"
24+
>
25+
<span className="material-symbols-outlined">menu</span>
26+
</label>
27+
</div>
28+
</nav>
29+
<div className="h-full max-h-full overflow-y-auto w-full md:p-10 p-4 flex flex-row gap-10 items-start justify-center">
30+
<div className="h-full max-h-full lg:w-[70%] w-full flex flex-col items-center justify-start">
31+
<div className="w-full h-full max-h-full fle flex-col items-center justify-center">
32+
{children}
33+
</div>
34+
</div>
35+
<div className="lg:flex hidden w-[30%] flex-col items-stretch justify-start gap-4">
36+
<SearchInput />
37+
<FollowSuggestions />
38+
</div>
39+
</div>
40+
<Dock hasUnreadNotifications={unreadCount > 0} />
41+
</div>
42+
<DrawerSide unreadCount={unreadCount} />
43+
</div>
44+
);
45+
};
46+
47+
const DrawerSide = ({ unreadCount }: { unreadCount: number }) => {
48+
return (
49+
<div className="drawer-side">
50+
<label
51+
htmlFor="my-drawer"
52+
aria-label="close sidebar"
53+
className="drawer-overlay"
54+
></label>
55+
<ul className="menu bg-base-200 min-h-full w-60 p-4">
56+
<li>
57+
<HomeLink />
58+
</li>
59+
<li>
60+
<PopularLink />
61+
</li>
62+
<li>
63+
<NotificationsLink>
64+
{unreadCount > 0 && (
65+
<div className="badge badge-primary badge-xs position-absolute left-23">
66+
{unreadCount}
67+
</div>
68+
)}
69+
</NotificationsLink>
70+
</li>
71+
<li>
72+
<BookmarksLink />
73+
</li>
74+
<li>
75+
<ProfileLink />
76+
</li>
77+
</ul>
78+
</div>
79+
);
80+
};
81+
82+
const Dock = ({
83+
hasUnreadNotifications,
84+
}: {
85+
hasUnreadNotifications: boolean;
86+
}) => {
87+
return (
88+
<div className="dock md:hidden w-full">
89+
<button>
90+
<HomeLink />
91+
</button>
92+
93+
<button>
94+
<ExploreLink />
95+
</button>
96+
97+
<button>
98+
<AddLink />
99+
</button>
100+
101+
<button>
102+
<NotificationsLink>
103+
{hasUnreadNotifications && (
104+
<div className="badge badge-primary h-[0.25rem] w-[0.25rem] p-[0.25rem] absolute left-[70%] top-[5%]" />
105+
)}
106+
</NotificationsLink>
107+
</button>
108+
109+
<button>
110+
<ProfileLink />
111+
</button>
112+
</div>
113+
);
114+
};
115+
116+
const HomeLink = () => {
117+
return <NavLink href="/home" icon="home" label="Home" />;
118+
};
119+
120+
const PopularLink = () => {
121+
return <NavLink href="/explore" icon="whatshot" label="Popular" />;
122+
};
123+
124+
const ExploreLink = () => {
125+
return <NavLink href="/explore" icon="search" />;
126+
};
127+
128+
const NotificationsLink = ({ children }: { children?: React.ReactNode }) => {
129+
return (
130+
<NavLink href="/notifications" icon="notifications" label="Notifications">
131+
{children}
132+
</NavLink>
133+
);
134+
};
135+
136+
const ProfileLink = () => {
137+
return <NavLink href="/profile" icon="account_circle" label="Profile" />;
138+
};
139+
140+
const AddLink = () => {
141+
return <NavLink href="/activity-compose" icon="add" label="Add" />;
142+
};
143+
144+
const BookmarksLink = () => {
145+
return <NavLink href="/bookmarks" icon="bookmark" label="Bookmarks" />;
146+
};
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
'use client';
2+
3+
import {
4+
useCreateFeedsClient,
5+
StreamFeeds,
6+
FeedsClient,
7+
} from '@stream-io/feeds-react-sdk';
8+
import { AppSkeleton } from './AppSkeleton';
9+
import { OwnFeedsContextProvider } from './own-feeds-context';
10+
import { FollowSuggestionsContextProvider } from './follow-suggestions-context';
11+
import { generateUsername } from 'unique-username-generator';
12+
import { useEffect, useMemo, type PropsWithChildren } from 'react';
13+
import { useSearchParams, useRouter } from 'next/navigation';
14+
import { LoadingIndicator } from './components/utility/LoadingIndicator';
15+
16+
export const ClientApp = ({ children }: PropsWithChildren) => {
17+
const searchParams = useSearchParams();
18+
const router = useRouter();
19+
20+
const API_KEY = process.env.NEXT_PUBLIC_API_KEY;
21+
const userIdFromUrl = searchParams.get('user_id');
22+
const USER_ID = useMemo(
23+
() =>
24+
process.env.NEXT_PUBLIC_USER_ID ?? userIdFromUrl ?? generateUsername('-'),
25+
[userIdFromUrl],
26+
);
27+
28+
// Set user_id as URL parameter if not already present
29+
useEffect(() => {
30+
if (!userIdFromUrl && USER_ID) {
31+
const urlParams = new URLSearchParams(searchParams.toString());
32+
urlParams.set('user_id', USER_ID);
33+
router.replace(`?${urlParams.toString()}`);
34+
}
35+
}, [userIdFromUrl, USER_ID, searchParams, router]);
36+
37+
const CURRENT_USER = useMemo(
38+
() => ({
39+
id: USER_ID,
40+
name: process.env.NEXT_PUBLIC_USER_NAME ?? USER_ID,
41+
token: process.env.NEXT_PUBLIC_USER_TOKEN
42+
? process.env.NEXT_PUBLIC_USER_TOKEN
43+
: process.env.NEXT_PUBLIC_TOKEN_URL
44+
? () =>
45+
fetch(
46+
`${process.env.NEXT_PUBLIC_TOKEN_URL}&user_id=${USER_ID}`,
47+
).then((res) => res.json().then((data) => data.token))
48+
: new FeedsClient(API_KEY!).devToken(USER_ID),
49+
}),
50+
[USER_ID, API_KEY],
51+
);
52+
53+
const client = useCreateFeedsClient({
54+
apiKey: API_KEY!,
55+
tokenOrProvider: CURRENT_USER.token,
56+
userData: {
57+
id: CURRENT_USER.id,
58+
name: CURRENT_USER.name,
59+
},
60+
});
61+
62+
if (!client) {
63+
return (
64+
<div className="flex items-center justify-center h-screen">
65+
<LoadingIndicator />
66+
</div>
67+
);
68+
}
69+
70+
return (
71+
<StreamFeeds client={client}>
72+
<OwnFeedsContextProvider>
73+
<FollowSuggestionsContextProvider>
74+
<AppSkeleton>{children}</AppSkeleton>
75+
</FollowSuggestionsContextProvider>
76+
</OwnFeedsContextProvider>
77+
</StreamFeeds>
78+
);
79+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use client';
2+
3+
import { StreamFeed } from '@stream-io/feeds-react-sdk';
4+
import { ActivityComposer } from '../components/activity/ActivityComposer';
5+
import { useOwnFeedsContext } from '../own-feeds-context';
6+
7+
export default function ActivityComposePage() {
8+
const { ownFeed } = useOwnFeedsContext();
9+
10+
if (!ownFeed) {
11+
return null;
12+
}
13+
14+
return (
15+
<StreamFeed feed={ownFeed}>
16+
<ActivityComposer />
17+
</StreamFeed>
18+
);
19+
}

0 commit comments

Comments
 (0)