mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-11 10:46:25 +03:00
community: convert realtime poll sample app to hooks (close #7675)
GITHUB_PR_NUMBER: 7694 GITHUB_PR_URL: https://github.com/hasura/graphql-engine/pull/7694 PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2620 Co-authored-by: andykcom <532952+andykcom@users.noreply.github.com> GitOrigin-RevId: 428d9a17468f18376957963a152afb11d176a39c
This commit is contained in:
parent
44977bdf9d
commit
0e6bcfdba4
@ -16,12 +16,13 @@ hosted on GitHub pages and the Postgres+GraphQL Engine is running on Postgres.
|
||||
- Checkout the [live app](https://realtime-poll.demo.hasura.app/).
|
||||
- Explore the backend using [Hasura
|
||||
Console](https://realtime-poll.hasura.app/console).
|
||||
|
||||
|
||||
# Running the app yourself
|
||||
|
||||
- Deploy GraphQL Engine on Hasura Cloud and setup PostgreSQL via Heroku:
|
||||
|
||||
|
||||
[![Deploy to Hasura Cloud](https://graphql-engine-cdn.hasura.io/img/deploy_to_hasura.png)](https://cloud.hasura.io/signup)
|
||||
|
||||
- Get the Hasura app URL (say `realtime-poll.hasura.app`)
|
||||
- Clone this repo:
|
||||
```bash
|
||||
@ -39,10 +40,10 @@ hosted on GitHub pages and the Postgres+GraphQL Engine is running on Postgres.
|
||||
hasura migrate apply
|
||||
hasura metadata reload
|
||||
```
|
||||
- Edit `HASURA_GRAPHQL_ENGINE_HOSTNAME` in `src/apollo.js` and set it to the
|
||||
- Edit `GRAPHQL_ENDPOINT` in `src/apollo.js` and set it to the
|
||||
Hasura app URL:
|
||||
```js
|
||||
export const HASURA_GRAPHQL_ENGINE_HOSTNAME = 'realtime-poll.hasura.app';
|
||||
export const GRAPHQL_ENDPOINT = "realtime-poll.hasura.app";
|
||||
```
|
||||
- Run the app (go the root of repo):
|
||||
```bash
|
||||
|
@ -3,21 +3,14 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"apollo-boost": "^0.4.7",
|
||||
"apollo-cache-inmemory": "^1.2.7",
|
||||
"apollo-client": "^2.3.8",
|
||||
"apollo-link": "^1.2.2",
|
||||
"apollo-link-http": "^1.5.4",
|
||||
"apollo-link-ws": "^1.0.8",
|
||||
"apollo-utilities": "^1.0.18",
|
||||
"graphql": "^15.0.0",
|
||||
"react": "^16.4.2",
|
||||
"react-apollo": "^3.1.4",
|
||||
"@apollo/client": "^3.4.16",
|
||||
"graphql": "^15.6.0",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap": "^1.0.0",
|
||||
"react-dom": "^16.4.2",
|
||||
"react-google-charts": "^3.0.5",
|
||||
"react-scripts": "^3.4.1",
|
||||
"subscriptions-transport-ws": "^0.9.14"
|
||||
"subscriptions-transport-ws": "^0.9.19"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
@ -3,22 +3,19 @@
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
/* animation: App-logo-spin infinite 20s linear;
|
||||
height: 80px; */
|
||||
width: 90px;
|
||||
display: inline-block;
|
||||
margin-right: 50px;
|
||||
}
|
||||
.displayFlex
|
||||
{
|
||||
.displayFlex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.hasura-logo a:hover {
|
||||
text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
.hasura-logo img {
|
||||
height: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.App-header {
|
||||
background-color: #222;
|
||||
@ -71,13 +68,17 @@ pre {
|
||||
width: 100%;
|
||||
}
|
||||
@keyframes App-logo-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.footer-small-text {
|
||||
padding-top: 10px;
|
||||
font-size: 0.8em;
|
||||
padding-top: 10px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.online-users {
|
||||
@ -87,22 +88,22 @@ pre {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.online-users .alert {
|
||||
margin-bottom: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.poll-result-chart-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
min-height: 350px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
min-height: 350px;
|
||||
}
|
||||
|
||||
@media(max-width: 767px) {
|
||||
.displayFlex {
|
||||
display: block;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.displayFlex {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.hasura-logo img {
|
||||
|
@ -1,66 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import logo from './img/logo-white.svg';
|
||||
import './App.css';
|
||||
import { ApolloProvider } from 'react-apollo';
|
||||
import client from './apollo';
|
||||
import Poll from './Poll';
|
||||
import { getUserId } from './session';
|
||||
import { GraphQL } from './GraphQL';
|
||||
import { Users } from './Users';
|
||||
|
||||
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { loading: true, userId: '' };
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
getUserId().then((userId) => {
|
||||
this.setState({ loading: false, userId });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
// if (this.state.loading) return <p>Loading...</p>;
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<div className="App">
|
||||
|
||||
<header className="App-header displayFlex">
|
||||
<div className="container displayFlex">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<h1 className="App-title">Realtime Poll</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<Users />
|
||||
|
||||
<Poll userId={this.state.userId} />
|
||||
|
||||
<GraphQL />
|
||||
|
||||
<footer className="App-footer displayFlex">
|
||||
<div className="container hasura-logo">
|
||||
<a href="https://hasura.io" target="_blank" rel="noopener noreferrer">
|
||||
<img className="hasura-logo" alt="hasura logo" src="https://graphql-engine-cdn.hasura.io/img/powered_by_hasura_black_200px.png" />
|
||||
</a>
|
||||
|
|
||||
<a href="https://realtime-poll.hasura.app/console" target="_blank">
|
||||
Backend
|
||||
</a>
|
||||
|
|
||||
<a href="https://github.com/hasura/graphql-engine/tree/master/community/sample-apps/realtime-poll" target="_blank" rel="noopener noreferrer">
|
||||
Source
|
||||
</a>
|
||||
<div className="footer-small-text"><span>(The database resets every 24 hours)</span></div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</ApolloProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
37
community/sample-apps/realtime-poll/src/App.jsx
Normal file
37
community/sample-apps/realtime-poll/src/App.jsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { ApolloProvider } from "@apollo/client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import client from "./apollo";
|
||||
import "./App.css";
|
||||
import { Footer, Header } from "./Components";
|
||||
import { GraphQLQueryList } from "./GraphQL";
|
||||
import { Poll } from "./Poll";
|
||||
import { getUserId } from "./session";
|
||||
import { Users } from "./Users";
|
||||
|
||||
const App = () => {
|
||||
const defaultState = { loading: true, userId: "" };
|
||||
const [state, setState] = useState(defaultState);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserId = async () => {
|
||||
const userId = await getUserId();
|
||||
setState({ loading: false, userId });
|
||||
};
|
||||
|
||||
fetchUserId();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<div className="App">
|
||||
<Header />
|
||||
<Users />
|
||||
<Poll userId={state.userId} />
|
||||
<GraphQLQueryList />
|
||||
<Footer />
|
||||
</div>
|
||||
</ApolloProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
49
community/sample-apps/realtime-poll/src/Components.jsx
Normal file
49
community/sample-apps/realtime-poll/src/Components.jsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import logo from "./img/logo-white.svg";
|
||||
|
||||
// State indicator components
|
||||
export const Loading = () => <div>Loading...</div>;
|
||||
export const Error = ({ message }) => (
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<b>Error:</b> {message}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Layout components
|
||||
export const Header = () => (
|
||||
<header className="App-header displayFlex">
|
||||
<div className="container displayFlex">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<h1 className="App-title">Realtime Poll</h1>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
export const Footer = () => (
|
||||
<footer className="App-footer displayFlex">
|
||||
<div className="container hasura-logo">
|
||||
<a href="https://hasura.io" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
className="hasura-logo"
|
||||
alt="hasura logo"
|
||||
src="https://graphql-engine-cdn.hasura.io/img/powered_by_hasura_black_200px.png"
|
||||
/>
|
||||
</a>
|
||||
|
|
||||
<a href="https://realtime-poll.hasura.app/console" target="_blank">
|
||||
Backend
|
||||
</a>
|
||||
|
|
||||
<a
|
||||
href="https://github.com/hasura/graphql-engine/tree/master/community/sample-apps/realtime-poll"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Source
|
||||
</a>
|
||||
<div className="footer-small-text">
|
||||
<span>(The database resets every 24 hours)</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
@ -1,115 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Card } from 'react-bootstrap';
|
||||
|
||||
const QUERY_GET_POLL = `
|
||||
query {
|
||||
poll (limit: 10) {
|
||||
id
|
||||
question
|
||||
options (order_by: {id:desc}){
|
||||
id
|
||||
text
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const MUTATION_VOTE = `
|
||||
mutation vote($optionId: uuid!, $userId: uuid!) {
|
||||
insert_vote(objects:[{
|
||||
option_id: $optionId,
|
||||
created_by_user_id: $userId
|
||||
}]) {
|
||||
returning {
|
||||
id
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const SUBSCRIPTION_RESULT = `
|
||||
subscription getResult($pollId: uuid!) {
|
||||
poll_results (
|
||||
order_by: {option_id:desc},
|
||||
where: { poll_id: {_eq: $pollId} }
|
||||
) {
|
||||
option_id
|
||||
option { id text }
|
||||
votes
|
||||
}
|
||||
}`;
|
||||
|
||||
const SUBSCRIPTION_ONLINE_USERS = `
|
||||
subscription getOnlineUsersCount {
|
||||
online_users {
|
||||
count
|
||||
}
|
||||
}`;
|
||||
|
||||
const MUTATION_MARK_USER_ONLINE = `
|
||||
mutation userOnline($uuid: uuid) {
|
||||
update_user(
|
||||
where: {id: {_eq: $uuid}},
|
||||
_set : { online_ping: true }
|
||||
) {
|
||||
affected_rows
|
||||
returning {
|
||||
last_seen_at
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const MUTATION_NEW_USER = `
|
||||
mutation newUser($uuid: uuid) {
|
||||
insert_user (
|
||||
objects:[{ id: $uuid }]
|
||||
) {
|
||||
returning {
|
||||
id
|
||||
created_at
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const GraphQL = () => (
|
||||
<div className="container">
|
||||
<div className="col-md-12 cardGraphQL">
|
||||
<Card>
|
||||
<Card.Header>GraphQL Queries/Mutations/Subscriptions in this page</Card.Header>
|
||||
<Card.Body>
|
||||
<div className="row">
|
||||
<div className="col-md-4">
|
||||
Get the Poll question and options:
|
||||
<pre>{QUERY_GET_POLL}</pre>
|
||||
|
||||
Create a new user:
|
||||
<pre>{MUTATION_NEW_USER}</pre>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
Cast a vote:
|
||||
<pre>{MUTATION_VOTE}</pre>
|
||||
|
||||
Mark user online:
|
||||
<pre>{MUTATION_MARK_USER_ONLINE}</pre>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
Show live results:
|
||||
<pre>{SUBSCRIPTION_RESULT}</pre>
|
||||
|
||||
Get real-time number of users:
|
||||
<pre>{SUBSCRIPTION_ONLINE_USERS}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export {
|
||||
GraphQL,
|
||||
QUERY_GET_POLL,
|
||||
MUTATION_VOTE,
|
||||
SUBSCRIPTION_RESULT,
|
||||
SUBSCRIPTION_ONLINE_USERS,
|
||||
MUTATION_MARK_USER_ONLINE,
|
||||
MUTATION_NEW_USER,
|
||||
};
|
118
community/sample-apps/realtime-poll/src/GraphQL.jsx
Normal file
118
community/sample-apps/realtime-poll/src/GraphQL.jsx
Normal file
@ -0,0 +1,118 @@
|
||||
import gql from "graphql-tag";
|
||||
import React from "react";
|
||||
import { Card } from "react-bootstrap";
|
||||
|
||||
const QUERY_GET_POLL = gql`
|
||||
query {
|
||||
poll(limit: 10) {
|
||||
id
|
||||
question
|
||||
options(order_by: { id: desc }) {
|
||||
id
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const MUTATION_VOTE = gql`
|
||||
mutation vote($optionId: uuid!, $userId: uuid!) {
|
||||
insert_vote(
|
||||
objects: [{ option_id: $optionId, created_by_user_id: $userId }]
|
||||
) {
|
||||
returning {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const SUBSCRIPTION_RESULT = gql`
|
||||
subscription getResult($pollId: uuid!) {
|
||||
poll_results(
|
||||
order_by: { option_id: desc }
|
||||
where: { poll_id: { _eq: $pollId } }
|
||||
) {
|
||||
option_id
|
||||
option {
|
||||
id
|
||||
text
|
||||
}
|
||||
votes
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const SUBSCRIPTION_ONLINE_USERS = gql`
|
||||
subscription getOnlineUsersCount {
|
||||
online_users {
|
||||
count
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const MUTATION_MARK_USER_ONLINE = gql`
|
||||
mutation userOnline($uuid: uuid) {
|
||||
update_user(where: { id: { _eq: $uuid } }, _set: { online_ping: true }) {
|
||||
affected_rows
|
||||
returning {
|
||||
last_seen_at
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const MUTATION_NEW_USER = gql`
|
||||
mutation newUser($uuid: uuid) {
|
||||
insert_user(objects: [{ id: $uuid }]) {
|
||||
returning {
|
||||
id
|
||||
created_at
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const GraphQLQueryList = () => (
|
||||
<div className="container">
|
||||
<div className="col-md-12 cardGraphQL">
|
||||
<Card>
|
||||
<Card.Header>
|
||||
GraphQL Queries/Mutations/Subscriptions in this page
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<div className="row">
|
||||
<div className="col-md-4">
|
||||
Get the Poll question and options:
|
||||
<pre>{QUERY_GET_POLL.loc.source.body}</pre>
|
||||
Create a new user:
|
||||
<pre>{MUTATION_NEW_USER.loc.source.body}</pre>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
Cast a vote:
|
||||
<pre>{MUTATION_VOTE.loc.source.body}</pre>
|
||||
Mark user online:
|
||||
<pre>{MUTATION_MARK_USER_ONLINE.loc.source.body}</pre>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
Show live results:
|
||||
<pre>{SUBSCRIPTION_RESULT.loc.source.body}</pre>
|
||||
Get real-time number of users:
|
||||
<pre>{SUBSCRIPTION_ONLINE_USERS.loc.source.body}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export {
|
||||
GraphQLQueryList,
|
||||
QUERY_GET_POLL,
|
||||
MUTATION_VOTE,
|
||||
SUBSCRIPTION_RESULT,
|
||||
SUBSCRIPTION_ONLINE_USERS,
|
||||
MUTATION_MARK_USER_ONLINE,
|
||||
MUTATION_NEW_USER,
|
||||
};
|
@ -1,137 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Query, Mutation } from 'react-apollo';
|
||||
import gql from 'graphql-tag';
|
||||
import { Button, Form } from 'react-bootstrap';
|
||||
import { Result } from './Result';
|
||||
import { QUERY_GET_POLL, MUTATION_VOTE } from './GraphQL';
|
||||
|
||||
class PollQuestion extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
optionId: '',
|
||||
pollId: props.poll.id,
|
||||
voteBtnText: '🗳 Vote',
|
||||
voteBtnStyle: 'primary'
|
||||
};
|
||||
}
|
||||
|
||||
handleOptionChange = (e) => {
|
||||
this.setState({
|
||||
optionId: e.currentTarget.value
|
||||
});
|
||||
}
|
||||
|
||||
onMutationCompleted = () => {
|
||||
this.setState({
|
||||
voteBtnText: '👍 Done',
|
||||
voteBtnStyle: 'success'
|
||||
});
|
||||
// re-authorize to vote after 5 seconds
|
||||
window.setTimeout(() => {
|
||||
this.setState({
|
||||
voteBtnText: '🗳️ Vote',
|
||||
voteBtnStyle: 'primary'
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
onMutationError = () => {
|
||||
this.setState({
|
||||
voteBtnText: 'Error 😞 Try again',
|
||||
voteBtnStyle: 'danger'
|
||||
});
|
||||
}
|
||||
|
||||
handlesubmitVote = (e, vote) => {
|
||||
e.preventDefault();
|
||||
if (!this.state.optionId) {
|
||||
this.setState({
|
||||
voteBtnText: '✋ Select an option and try again',
|
||||
voteBtnStyle: 'warning'
|
||||
});
|
||||
return
|
||||
}
|
||||
this.setState({
|
||||
voteBtnText: '🗳️ Submitting',
|
||||
voteBtnStyle: 'info'
|
||||
});
|
||||
vote({
|
||||
variables: {
|
||||
optionId: this.state.optionId,
|
||||
userId: this.props.userId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Mutation
|
||||
mutation={gql`${MUTATION_VOTE}`}
|
||||
onCompleted={this.onMutationCompleted}
|
||||
onError={this.onMutationError}
|
||||
>
|
||||
{(vote) => (
|
||||
<div className="textLeft">
|
||||
<h3>{this.props.poll.question}</h3>
|
||||
<Form className="pollForm textLeft" onSubmit={e => { this.handlesubmitVote(e, vote) }}>
|
||||
{
|
||||
this.props.poll.options.map(option => (
|
||||
|
||||
<Form.Check
|
||||
custom
|
||||
type="radio"
|
||||
name="voteCandidate"
|
||||
id={option.id}
|
||||
key={option.id}
|
||||
value={option.id}
|
||||
label={option.text}
|
||||
onChange={this.handleOptionChange}
|
||||
/>
|
||||
|
||||
))
|
||||
}
|
||||
<Button className="voteBtn info" variant={this.state.voteBtnStyle} type="submit">
|
||||
{this.state.voteBtnText}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
</Mutation>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const Poll = ({ userId }) => (
|
||||
<div>
|
||||
<Query query={gql`${QUERY_GET_POLL}`}>
|
||||
{({ loading, error, data }) => {
|
||||
if (loading) return <p>Loading...</p>;
|
||||
if (error) {
|
||||
return <div class="alert alert-danger" role="alert"><b>Error:</b> ${error.message}</div>;
|
||||
}
|
||||
return (
|
||||
<div className="container">
|
||||
{
|
||||
data.poll.map(poll => (
|
||||
<div key={poll.id} className="pollWrapper wd100">
|
||||
<div className="displayFlex">
|
||||
<div className="col-md-4 pollSlider">
|
||||
<PollQuestion poll={poll} userId={userId} />
|
||||
</div>
|
||||
<div className="col-md-8 pollresult">
|
||||
<Result pollId={poll.id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Query>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Poll;
|
128
community/sample-apps/realtime-poll/src/Poll.jsx
Normal file
128
community/sample-apps/realtime-poll/src/Poll.jsx
Normal file
@ -0,0 +1,128 @@
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { Error, Loading } from "./Components";
|
||||
import { MUTATION_VOTE, QUERY_GET_POLL } from "./GraphQL";
|
||||
import { Result } from "./Result";
|
||||
|
||||
const PollQuestion = ({ poll, userId }) => {
|
||||
const defaultState = {
|
||||
optionId: "",
|
||||
pollId: poll.id,
|
||||
voteBtnText: "🗳 Vote",
|
||||
voteBtnStyle: "primary",
|
||||
};
|
||||
const [state, setState] = useState(defaultState);
|
||||
const [vote, { data, loading, error }] = useMutation(MUTATION_VOTE);
|
||||
|
||||
const handlesubmitVote = (e) => {
|
||||
e.preventDefault();
|
||||
if (!state.optionId) {
|
||||
setState({
|
||||
voteBtnText: "✋ Select an option and try again",
|
||||
voteBtnStyle: "warning",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState({
|
||||
voteBtnText: "🗳️ Submitting",
|
||||
voteBtnStyle: "info",
|
||||
});
|
||||
|
||||
vote({
|
||||
variables: {
|
||||
optionId: state.optionId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// To-do: use cleanup
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setState({
|
||||
voteBtnText: "👍 Done",
|
||||
voteBtnStyle: "success",
|
||||
});
|
||||
|
||||
// Re-authorize to vote after 5 seconds
|
||||
let timer = setTimeout(() => {
|
||||
setState({
|
||||
voteBtnText: "🗳️ Vote",
|
||||
voteBtnStyle: "primary",
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
setState({
|
||||
voteBtnText: "Error 😞 Try again",
|
||||
voteBtnStyle: "danger",
|
||||
});
|
||||
}
|
||||
}, [data, error]);
|
||||
|
||||
const handleOptionChange = (e) => {
|
||||
const optionId = e.currentTarget.value;
|
||||
setState((prev) => ({ ...prev, optionId }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="textLeft">
|
||||
<h3>{poll.question}</h3>
|
||||
<Form
|
||||
className="pollForm textLeft"
|
||||
onSubmit={(e) => {
|
||||
handlesubmitVote(e);
|
||||
}}
|
||||
>
|
||||
{poll.options.map(({ id, text }) => (
|
||||
<Form.Check
|
||||
custom
|
||||
type="radio"
|
||||
name="voteCandidate"
|
||||
id={id}
|
||||
key={id}
|
||||
value={id}
|
||||
label={text}
|
||||
onChange={handleOptionChange}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
className="voteBtn info"
|
||||
variant={state.voteBtnStyle}
|
||||
type="submit"
|
||||
>
|
||||
{state.voteBtnText}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Poll = ({ userId }) => {
|
||||
const { data, loading, error } = useQuery(QUERY_GET_POLL);
|
||||
|
||||
if (loading) return <Loading />;
|
||||
if (error) return <Error message={error.message} />;
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
{data?.poll.map((poll) => (
|
||||
<div key={poll.id} className="pollWrapper wd100">
|
||||
<div className="displayFlex">
|
||||
<div className="col-md-4 pollSlider">
|
||||
<PollQuestion poll={poll} userId={userId} />
|
||||
</div>
|
||||
<div className="col-md-8 pollresult">
|
||||
<Result pollId={poll.id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,58 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Subscription,
|
||||
} from 'react-apollo';
|
||||
import { Chart } from 'react-google-charts';
|
||||
import gql from 'graphql-tag';
|
||||
import { SUBSCRIPTION_RESULT } from './GraphQL'
|
||||
|
||||
const renderChart = (data) => {
|
||||
const d = [
|
||||
['Option', 'No. of votes', { role: 'annotation' }, { role: 'style' }],
|
||||
];
|
||||
for (var r of data.poll_results) {
|
||||
console.log(r);
|
||||
d.push([r.option.text, parseInt(r.votes, 10), parseInt(r.votes, 10), 'color: #4285f4']);
|
||||
}
|
||||
|
||||
return (
|
||||
<Chart className="poll-result-chart-container"
|
||||
chartType="BarChart"
|
||||
loader={<div>Loading Chart</div>}
|
||||
data={d}
|
||||
options={{
|
||||
height: '100%',
|
||||
chart: {
|
||||
title: 'Realtime results',
|
||||
},
|
||||
legend: { position: 'none' },
|
||||
animation: {
|
||||
duration: 1000,
|
||||
easing: 'out',
|
||||
startup: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const Result = (pollId) => (
|
||||
<Subscription subscription={gql`${SUBSCRIPTION_RESULT}`} variables={pollId}>
|
||||
{({ loading, error, data }) => {
|
||||
if (loading) return <p>Loading...</p>;
|
||||
if (error) return <p>Error :</p>;
|
||||
return (
|
||||
<div>
|
||||
{data.poll_results.length > 0
|
||||
?
|
||||
<div>
|
||||
{renderChart(data)}
|
||||
</div>
|
||||
:
|
||||
<p>No result</p>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Subscription>
|
||||
)
|
56
community/sample-apps/realtime-poll/src/Result.jsx
Normal file
56
community/sample-apps/realtime-poll/src/Result.jsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { useSubscription } from "@apollo/client";
|
||||
import React from "react";
|
||||
import { Chart } from "react-google-charts";
|
||||
import { Error, Loading } from "./Components";
|
||||
import { SUBSCRIPTION_RESULT } from "./GraphQL";
|
||||
|
||||
export const Result = ({ pollId }) => {
|
||||
const { data, loading, error } = useSubscription(SUBSCRIPTION_RESULT, {
|
||||
variables: { pollId },
|
||||
});
|
||||
|
||||
const hasResults = data?.poll_results.length > 0;
|
||||
|
||||
if (loading) return <Loading />;
|
||||
if (error) return <Error message={error.message} />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{hasResults ? <PollChart data={data?.poll_results} /> : <p>No result</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PollChart = ({ data }) => {
|
||||
const COLOR = "color: #4285f4";
|
||||
const d = [
|
||||
["Option", "No. of votes", { role: "annotation" }, { role: "style" }],
|
||||
];
|
||||
|
||||
data.forEach(({ option, votes }) =>
|
||||
d.push([option.text, parseInt(votes), parseInt(votes), COLOR])
|
||||
);
|
||||
|
||||
return (
|
||||
<Chart
|
||||
className="poll-result-chart-container"
|
||||
chartType="BarChart"
|
||||
loader={<div>Loading Chart</div>}
|
||||
data={d}
|
||||
options={chartOptions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const chartOptions = {
|
||||
height: "100%",
|
||||
chart: {
|
||||
title: "Realtime results",
|
||||
},
|
||||
legend: { position: "none" },
|
||||
animation: {
|
||||
duration: 1000,
|
||||
easing: "out",
|
||||
startup: true,
|
||||
},
|
||||
};
|
@ -1,27 +0,0 @@
|
||||
import React from 'react';
|
||||
import gql from 'graphql-tag';
|
||||
import { Subscription } from 'react-apollo';
|
||||
import {
|
||||
Alert,
|
||||
} from 'react-bootstrap';
|
||||
import { SUBSCRIPTION_ONLINE_USERS } from './GraphQL';
|
||||
|
||||
export const Users = () => (
|
||||
<Subscription subscription={gql`${SUBSCRIPTION_ONLINE_USERS}`}>
|
||||
{({ loading, error, data }) => {
|
||||
if (loading) return <span>Loading...</span>;
|
||||
if (error) {
|
||||
return <div class="alert alert-danger" role="alert"><b>Error:</b> ${error.message}</div>;
|
||||
}
|
||||
return (
|
||||
<div className="displayFlex online-users">
|
||||
<div className="col-md-6">
|
||||
<Alert variant="info">
|
||||
<span role="img" aria-label="online users">👥</span> Online users: {data.online_users[0].count}
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Subscription>
|
||||
)
|
26
community/sample-apps/realtime-poll/src/Users.jsx
Normal file
26
community/sample-apps/realtime-poll/src/Users.jsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { useSubscription } from "@apollo/client";
|
||||
import React from "react";
|
||||
import { Alert } from "react-bootstrap";
|
||||
import { Error, Loading } from "./Components";
|
||||
import { SUBSCRIPTION_ONLINE_USERS } from "./GraphQL";
|
||||
|
||||
export const Users = () => {
|
||||
const { data, loading, error } = useSubscription(SUBSCRIPTION_ONLINE_USERS);
|
||||
const { count } = data?.online_users[0] || {};
|
||||
|
||||
if (loading) return <Loading />;
|
||||
if (error) return <Error message={error.message} />;
|
||||
|
||||
return (
|
||||
<div className="displayFlex online-users">
|
||||
<div className="col-md-6">
|
||||
<Alert variant="info">
|
||||
<span role="img" aria-label="online users">
|
||||
👥
|
||||
</span>{" "}
|
||||
Online users: {count}
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,52 +1,26 @@
|
||||
// Remove the apollo-boost import and change to this:
|
||||
import ApolloClient from "apollo-client";
|
||||
import { ApolloClient, HttpLink, InMemoryCache, split } from "@apollo/client";
|
||||
import { WebSocketLink } from "@apollo/client/link/ws";
|
||||
import { getMainDefinition } from "@apollo/client/utilities";
|
||||
|
||||
// Setup the network "links"
|
||||
import { WebSocketLink } from 'apollo-link-ws';
|
||||
import { HttpLink } from 'apollo-link-http';
|
||||
import { split } from 'apollo-link';
|
||||
import { getMainDefinition } from 'apollo-utilities';
|
||||
const scheme = (proto) =>
|
||||
window.location.protocol === "https:" ? `${proto}s` : proto;
|
||||
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||
|
||||
export const HASURA_GRAPHQL_ENGINE_HOSTNAME = 'realtime-poll.hasura.app';
|
||||
|
||||
const scheme = (proto) => {
|
||||
return window.location.protocol === 'https:' ? `${proto}s` : proto;
|
||||
}
|
||||
|
||||
const wsurl = `${scheme('ws')}://${HASURA_GRAPHQL_ENGINE_HOSTNAME}/v1/graphql`;
|
||||
const httpurl = `${scheme('http')}://${HASURA_GRAPHQL_ENGINE_HOSTNAME}/v1/graphql`;
|
||||
|
||||
const wsLink = new WebSocketLink({
|
||||
uri: wsurl,
|
||||
options: {
|
||||
reconnect: true,
|
||||
}
|
||||
});
|
||||
|
||||
const httpLink = new HttpLink({
|
||||
uri: httpurl,
|
||||
});
|
||||
|
||||
const link = split(
|
||||
// split based on operation type
|
||||
({ query }) => {
|
||||
const { kind, operation } = getMainDefinition(query);
|
||||
return kind === 'OperationDefinition' && operation === 'subscription';
|
||||
},
|
||||
wsLink,
|
||||
httpLink,
|
||||
);
|
||||
|
||||
const createApolloClient = () => {
|
||||
return new ApolloClient({
|
||||
link,
|
||||
cache: new InMemoryCache()
|
||||
});
|
||||
const splitter = ({ query }) => {
|
||||
const { kind, operation } = getMainDefinition(query) || {};
|
||||
const isSubscription =
|
||||
kind === "OperationDefinition" && operation === "subscription";
|
||||
return isSubscription;
|
||||
};
|
||||
|
||||
const GRAPHQL_ENDPOINT = "realtime-poll.hasura.app";
|
||||
const cache = new InMemoryCache();
|
||||
const options = { reconnect: true };
|
||||
|
||||
const client = createApolloClient();
|
||||
const wsURI = `${scheme("ws")}://${GRAPHQL_ENDPOINT}/v1/graphql`;
|
||||
const httpurl = `${scheme("https")}://${GRAPHQL_ENDPOINT}/v1/graphql`;
|
||||
|
||||
const wsLink = new WebSocketLink({ uri: wsURI, options });
|
||||
const httpLink = new HttpLink({ uri: httpurl });
|
||||
const link = split(splitter, wsLink, httpLink);
|
||||
const client = new ApolloClient({ link, cache });
|
||||
export default client;
|
||||
|
@ -1,9 +1,5 @@
|
||||
import gql from 'graphql-tag';
|
||||
import client from './apollo';
|
||||
import {
|
||||
MUTATION_MARK_USER_ONLINE,
|
||||
MUTATION_NEW_USER,
|
||||
} from './GraphQL';
|
||||
import client from "./apollo";
|
||||
import { MUTATION_MARK_USER_ONLINE, MUTATION_NEW_USER } from "./GraphQL";
|
||||
|
||||
const newUUID = () => {
|
||||
const p8 = (s) => {
|
||||
@ -13,49 +9,40 @@ const newUUID = () => {
|
||||
return p8() + p8(true) + p8(true) + p8();
|
||||
};
|
||||
|
||||
|
||||
const getUserId = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// let uid = window.localStorage.getItem('uid');
|
||||
// if (!uid) {
|
||||
client.mutate({
|
||||
mutation: gql`${MUTATION_NEW_USER}`,
|
||||
variables: {
|
||||
uuid: newUUID(),
|
||||
}
|
||||
}).then(({ data }) => {
|
||||
if (data.insert_user.returning.length > 0) {
|
||||
const user = data.insert_user.returning[0];
|
||||
console.log("getUserId user.id:", user.id);
|
||||
// window.localStorage.setItem('uid', user.id);
|
||||
// window.localStorage.setItem('createdAt', user.created_at);
|
||||
reportUserOnline(user.id);
|
||||
resolve(user.id);
|
||||
} else {
|
||||
reject('no data');
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
reject(error);
|
||||
});
|
||||
// } else {
|
||||
// reportUserOnline(uid);
|
||||
// resolve(uid);
|
||||
// }
|
||||
const getUserId = async () => {
|
||||
const { data } = await client.mutate({
|
||||
mutation: MUTATION_NEW_USER,
|
||||
variables: { uuid: newUUID() },
|
||||
});
|
||||
|
||||
try {
|
||||
if (data?.insert_user.returning.length > 0) {
|
||||
const { id } = data.insert_user.returning[0] || {};
|
||||
|
||||
reportUserOnline(id);
|
||||
return id;
|
||||
} else {
|
||||
throw new Error(400);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error(400);
|
||||
}
|
||||
};
|
||||
|
||||
const reportUserOnline = (userId) => {
|
||||
window.setInterval(() => {
|
||||
client.mutate({
|
||||
mutation: gql`${MUTATION_MARK_USER_ONLINE}`,
|
||||
variables: {
|
||||
uuid: userId,
|
||||
},
|
||||
});
|
||||
window.setInterval(async () => {
|
||||
try {
|
||||
await client.mutate({
|
||||
mutation: MUTATION_MARK_USER_ONLINE,
|
||||
variables: {
|
||||
uuid: userId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
export {
|
||||
getUserId
|
||||
};
|
||||
export { getUserId };
|
||||
|
Loading…
Reference in New Issue
Block a user