Skip to content

Commit f771210

Browse files
sodicvincanger
andauthored
Add websockets example app (#1510)
Co-authored-by: vincanger <[email protected]>
1 parent 1162fed commit f771210

20 files changed

+515
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/.wasp/
2+
/.env.server
3+
/.env.client
4+
.DS_Store
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
File marking the root of Wasp project.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Using Websockets in Wasp
2+
3+
This is an example real-time, Websockets app built with Wasp in TypeScript to showcase the ease of use and integration of Websockets in Wasp. It's really NEAT!
4+
5+
[![wasp websockets app](image.png)](https://www.youtube.com/watch?v=Twy-2P0Co6M)
6+
7+
This app also includes Wasp's integrated auth and a voting system (again, neat!).
8+
9+
## Running the app
10+
11+
*If you get stuck at any point, feel free to join our [Discord server](https://discord.gg/rzdnErX) and ask questions there. We are happy to help!*
12+
13+
First, clone the this repo:
14+
```bash
15+
git clone https://github.com/wasp-lang/wasp.git
16+
```
17+
18+
Make sure you've downloaded and installed Wasp
19+
```bash
20+
curl -sSL https://get.wasp-lang.dev/installer.sh | sh
21+
```
22+
23+
Then navigate to the project directory
24+
```bash
25+
cd examples/websockets-realtime-voting
26+
```
27+
28+
```bash
29+
wasp db migrate-dev
30+
```
31+
32+
start the app! (this also installs all dependencies)
33+
```bash
34+
wasp start
35+
```
36+
37+
Check out the `src/server/websocket.ts` and `src/client/pages/MainPage.tsx` to see how Websockets are used in Wasp.
38+
39+
## Need Help?
40+
41+
Wasp Docs: https://wasp-lang.dev/docs
42+
43+
Feel free to join our [Discord server](https://discord.gg/rzdnErX) and ask questions there. We are happy to help!
794 KB
Loading
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
app whereDoWeEat {
2+
wasp: {
3+
version: "^0.11.6"
4+
},
5+
title: "where-do-we-eat",
6+
client: {
7+
rootComponent: import { Layout } from "@client/Layout.jsx",
8+
},
9+
auth: {
10+
userEntity: User,
11+
onAuthFailedRedirectTo: "/login",
12+
methods: {
13+
usernameAndPassword: {}
14+
}
15+
},
16+
dependencies: [
17+
("flowbite", "1.6.6"),
18+
("flowbite-react", "0.4.9")
19+
],
20+
webSocket: {
21+
fn: import { webSocketFn } from "@server/ws-server.js",
22+
}
23+
}
24+
25+
entity User {=psl
26+
id Int @id @default(autoincrement())
27+
username String @unique
28+
password String
29+
psl=}
30+
31+
route RootRoute { path: "/", to: MainPage }
32+
page MainPage {
33+
component: import Main from "@client/MainPage.tsx",
34+
authRequired: true
35+
}
36+
37+
route LoginRoute { path: "/login", to: LoginPage }
38+
page LoginPage {
39+
component: import { LoginPage } from "@client/pages/LoginPage.jsx"
40+
}
41+
42+
route RegisterRoute { path: "/signup", to: RegisterPage }
43+
page RegisterPage {
44+
component: import { SignupPage } from "@client/pages/SignupPage.jsx"
45+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
plugins: {
3+
tailwindcss: {},
4+
autoprefixer: {},
5+
},
6+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Ignore editor tmp files
2+
**/*~
3+
**/#*#
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// @ts-check
2+
import "./Main.css";
3+
4+
import { Flowbite, Dropdown, Navbar, Avatar } from "flowbite-react";
5+
import Logo from "./logo.png";
6+
import useAuth from "@wasp/auth/useAuth";
7+
import logout from "@wasp/auth/logout";
8+
9+
const customTheme = {
10+
button: {
11+
color: {
12+
primary: "bg-red-500 hover:bg-red-600",
13+
},
14+
},
15+
};
16+
17+
export const Layout = ({ children }) => {
18+
const { data: user } = useAuth();
19+
return (
20+
<Flowbite theme={{ theme: customTheme }}>
21+
<div className="p-8">
22+
<Navbar fluid rounded>
23+
<Navbar.Brand href="https://flowbite-react.com">
24+
<img
25+
alt="Flowbite React Logo"
26+
className="mr-3 h-6 sm:h-9"
27+
src={Logo}
28+
/>
29+
<span className="self-center whitespace-nowrap text-xl font-semibold dark:text-white">
30+
Undecisive Fox App
31+
</span>
32+
</Navbar.Brand>
33+
{user && (
34+
<div className="flex md:order-2">
35+
<Dropdown
36+
inline
37+
label={
38+
<Avatar
39+
alt="User settings"
40+
img={`https://xsgames.co/randomusers/avatar.php?g=female&username=${user.username}`}
41+
rounded
42+
/>
43+
}
44+
>
45+
<Dropdown.Header>
46+
<span className="block text-sm">{user.username}</span>
47+
</Dropdown.Header>
48+
<Dropdown.Item>Dashboard</Dropdown.Item>
49+
<Dropdown.Item>Settings</Dropdown.Item>
50+
<Dropdown.Item>Earnings</Dropdown.Item>
51+
<Dropdown.Divider />
52+
<Dropdown.Item onClick={logout}>Sign out</Dropdown.Item>
53+
</Dropdown>
54+
<Navbar.Toggle />
55+
</div>
56+
)}
57+
{/* <Navbar.Collapse>
58+
<Navbar.Link active href="#">
59+
<p>Home</p>
60+
</Navbar.Link>
61+
<Navbar.Link href="#">About</Navbar.Link>
62+
<Navbar.Link href="#">Services</Navbar.Link>
63+
<Navbar.Link href="#">Pricing</Navbar.Link>
64+
<Navbar.Link href="#">Contact</Navbar.Link>
65+
</Navbar.Collapse> */}
66+
</Navbar>
67+
<div className="grid place-items-center mt-8">{children}</div>
68+
</div>
69+
</Flowbite>
70+
);
71+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { useState, useMemo, useEffect } from "react";
2+
import { Button, Card } from "flowbite-react";
3+
import {
4+
useSocketListener,
5+
useSocket,
6+
ServerToClientPayload,
7+
} from "@wasp/webSocket";
8+
import useAuth from "@wasp/auth/useAuth";
9+
10+
const MainPage = () => {
11+
const { data: user } = useAuth();
12+
const [poll, setPoll] = useState<ServerToClientPayload<"updateState"> | null>(
13+
null
14+
);
15+
const totalVotes = useMemo(() => {
16+
return (
17+
poll?.options.reduce((acc, option) => acc + option.votes.length, 0) ?? 0
18+
);
19+
}, [poll]);
20+
21+
const { socket } = useSocket();
22+
23+
useSocketListener("updateState", (newState) => {
24+
setPoll(newState);
25+
});
26+
27+
useEffect(() => {
28+
socket.emit("askForStateUpdate");
29+
}, []);
30+
31+
function handleVote(optionId: number) {
32+
socket.emit("vote", optionId);
33+
}
34+
35+
return (
36+
<div className="w-full max-w-2xl mx-auto p-8">
37+
<h1 className="text-2xl font-bold">{poll?.question ?? "Loading..."}</h1>
38+
{poll && (
39+
<p className="leading-relaxed text-gray-500">
40+
Cast your vote for one of the options.
41+
</p>
42+
)}
43+
{poll && (
44+
<div className="mt-4 flex flex-col gap-4">
45+
{poll.options.map((option) => (
46+
<Card key={option.id} className="relative transition-all duration-300 min-h-[130px]">
47+
<div className="z-10">
48+
<div className="mb-2">
49+
<h2 className="text-xl font-semibold">{option.text}</h2>
50+
<p className="text-gray-700">{option.description}</p>
51+
</div>
52+
<div className="absolute bottom-5 right-5">
53+
{user && !option.votes.includes(user.username) ? (
54+
<Button onClick={() => handleVote(option.id)}>Vote</Button>
55+
) : (
56+
<Button disabled>Voted</Button>
57+
)}
58+
{!user}
59+
</div>
60+
{option.votes.length > 0 && (
61+
<div className="mt-2 flex gap-2 flex-wrap max-w-[75%]">
62+
{option.votes.map((vote) => (
63+
<div
64+
key={vote}
65+
className="py-1 px-3 bg-gray-100 rounded-lg flex items-center justify-center shadow text-sm"
66+
>
67+
<div className="w-2 h-2 bg-green-500 rounded-full mr-2"></div>
68+
<div className="text-gray-700">{vote}</div>
69+
</div>
70+
))}
71+
</div>
72+
)}
73+
</div>
74+
<div className="absolute top-5 right-5 p-2 text-sm font-semibold bg-gray-100 rounded-lg z-10">
75+
{option.votes.length} / {totalVotes}
76+
</div>
77+
<div
78+
className="absolute inset-0 bg-gradient-to-r from-yellow-400 to-orange-500 opacity-75 rounded-lg transition-all duration-300"
79+
style={{
80+
width: `${
81+
totalVotes > 0
82+
? (option.votes.length / totalVotes) * 100
83+
: 0
84+
}%`,
85+
}}
86+
></div>
87+
</Card>
88+
))}
89+
</div>
90+
)}
91+
</div>
92+
);
93+
};
94+
export default MainPage;

0 commit comments

Comments
 (0)