Skip to content

Commit

Permalink
Add the demo package to git (#143)
Browse files Browse the repository at this point in the history
  • Loading branch information
incepter authored Apr 2, 2023
1 parent 7477d34 commit 8010b86
Show file tree
Hide file tree
Showing 26 changed files with 705 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ __tests__/async-state

.scannerwork
memlab
packages/demo
#packages/demo
24 changes: 24 additions & 0 deletions packages/demo/react-async-states-simple-demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
13 changes: 13 additions & 0 deletions packages/demo/react-async-states-simple-demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root">My awesome splash screen</div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
26 changes: 26 additions & 0 deletions packages/demo/react-async-states-simple-demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "react-async-states-simple-demo",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"async-states": "workspace:^1.3.0",
"axios": "^1.3.4",
"react": "^18.2.0",
"react-async-states": "workspace:^1.3.0",
"react-dom": "^18.2.0",
"react-router-dom": "6.10.0"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^3.1.0",
"typescript": "4.9.5",
"vite": "^4.2.0"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions packages/demo/react-async-states-simple-demo/src/app/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import axios from "axios";

export const API = axios.create({
baseURL: "https://jsonplaceholder.typicode.com",
});
72 changes: 72 additions & 0 deletions packages/demo/react-async-states-simple-demo/src/app/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {api, createApplication, ProducerProps} from "react-async-states";
import {UserType} from "./users/types";
import {PostType} from "./posts/types";
import {API} from "./api";
import {bindAbort} from "../utils";

let goodUxConfig = {
skipPendingDelayMs: 300,
keepPendingForMs: 300,
}
let myApp = {
posts: {
search: api<PostType[], Error, never, [string]>(),
},
users: {
search: api<UserType[], Error, never, [string]>(),
findById: api<UserType, Error, never, [string]>(),
deleteUser: api<boolean, Error, never, [string]>(),
editUser: api<UserType, Error, never, [UserType]>(),
addNewPost: api<PostType, Error, never, [PostType]>(),
findUserPosts: api<UserType, Error, never, [string]>(),
},
auth: {
login: api<string, Error, never, [string, string]>({
eager: true,
config: goodUxConfig,
producer: loginProducer,
}),
current: api<UserType, Error, never, []>({
eager: true,
producer: currentUserProducer,
config: {
...goodUxConfig,
cacheConfig: {
enabled: true,
hash: () => localStorage.getItem("__principal_id__v1.0") as string
}
},
}),
},
}

export let app = createApplication<typeof myApp>(myApp)

// Static producers
async function loginProducer(props: ProducerProps<string, Error, never, [string, string]>) {
let [userId] = props.args
if (!+userId) {
throw new Error(`UserId ${userId} is not a number between 1 and 10`)
}
console.log('boom with', userId)
localStorage.setItem("__principal_id__v1.0", userId)
await app.auth.current().runp()
return userId
}

async function currentUserProducer(props: ProducerProps<UserType, Error, never, []>) {
let signal = bindAbort(props)
let currentUserId = localStorage.getItem("__principal_id__v1.0")
if (!currentUserId) {
throw new Error("No saved user")
}

let userDetails = await API.get<UserType>(`/users/${currentUserId}`, {signal});
return userDetails.data
}

export function logout() {
localStorage.removeItem("__principal_id__v1.0")
app.auth.current().invalidateCache()
app.auth.current().run()
}
14 changes: 14 additions & 0 deletions packages/demo/react-async-states-simple-demo/src/app/entry.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.App {
height: 100vh;
width: 100%;
display: flex;
align-items: flex-start;
flex-direction: column;
}
#root {
width: 100%;

}
.main {
width: 100%;
}
50 changes: 50 additions & 0 deletions packages/demo/react-async-states-simple-demo/src/app/entry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as React from 'react'
import {app, logout} from "./app";
import LoginPage from "./login/page";
import "./entry.css"
import {Link, Outlet } from 'react-router-dom';

function isUnauthorized(error) {
return error?.toString?.() === "Error: No saved user"
}

export function Component() {
let data;
try {
data = app.auth.current.use()
} catch (e) {
let knownError = isUnauthorized(e);
if (knownError) {
return <LoginPage/>
}
}

return (
<div className="App">
<div className="main">
<div>
Use The app as : <LoginPage/>
</div>
<hr />
<div>
<div>
<button onClick={logout}>Logout</button>
<details>
<summary>Using The app as user: {data!.id} - {data!.username}</summary>
<pre>{JSON.stringify(data, null, 4)}</pre>
</details>
</div>
<hr />
<nav style={{display: "flex", flexDirection: "column"}}>
<Link to="users">Users list</Link>
<Link to="posts">Posts list</Link>
</nav>
<hr />
<React.Suspense>
<Outlet />
</React.Suspense>
</div>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import * as React from "react";

type State = {
counter: number;
error: Error | null;
errorInfo: React.ErrorInfo | null;
};
type BoundaryProps = {
logErrors?: boolean;
children: React.ReactNode;
};
export default class DefaultErrorBoundary extends React.PureComponent<
BoundaryProps,
State
> {
constructor(props) {
super(props);
this.state = {
counter: 0,
error: null,
errorInfo: null,
};
this.retry = this.retry.bind(this);
}

componentDidCatch(error, errorInfo) {
this.setState((old) => ({
...old,
error,
errorInfo,
}));
if (this.props.logErrors) {
console.error("RetryableErrorBoundary error", { error, errorInfo });
}
}

retry() {
this.setState((old) => ({
error: null,
errorInfo: null,
counter: old.counter + 1,
}));
}

render() {
const { children } = this.props;
const { error, errorInfo, counter } = this.state;

if (errorInfo) {
return (
<div
style={{
padding: 8,
display: "flex",
alignItems: "center",
}}
>
<div
style={
{
// display: "flex",
// alignItems: "center",
// justifyContent: "space-between",
}
}
>
<h3>
<span>The following error occurred {this.state.counter + 1}</span>
<button
className="button secondary light"
style={{ marginLeft: 32 }}
onClick={this.retry}
>
Retry
</button>
</h3>

<details open>
<summary>{error?.toString?.()}</summary>
<button
className="button primary light"
style={{ marginLeft: 32 }}
onClick={() => copyErrorInfo(this.state)}
>
Copy to clipboard
</button>
<pre>{errorInfo.componentStack}</pre>
</details>
</div>
</div>
);
}
return <Ghost key={counter}>{children}</Ghost>;
}
}

function Ghost({ children }) {
return children;
}

function copyErrorInfo(state) {
if (!state.error) {
return;
}
try {
let messageToCopy = JSON.stringify(
{
app: navigator.platform,
userAgent: navigator.userAgent,
componentName: resolveComponentNameFromStackTrace(
state.errorInfo.componentStack
),
...state,
},
null,
4
);
navigator.clipboard.writeText(messageToCopy);
} catch (e) {
console.error("couldn't copy error info to clipboard", e);
}
}

export function resolveComponentNameFromStackTrace(
stack,
level = 0
): undefined | string {
if (!stack) {
return undefined;
}
// eslint-disable-next-line prefer-regex-literals
const regex = new RegExp(/at.(\w+).*$/, "gm");

let levelsCount = 0;

let match = regex.exec(stack);

while (levelsCount < level && match) {
match = regex.exec(stack);

levelsCount += 1;
}

return match?.[1];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, {FormEvent, FormEventHandler} from "react";
import {app} from "../app";

function login(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
let {elements} = (event.target as HTMLFormElement)
// @ts-ignore
let userId = elements.namedItem("username").value
// @ts-ignore
// let password = elements.namedItem("password").value
app.auth.login().run(userId, "nopassword")
}


export default function LoginPage() {
let {state: {status}} = app.auth.login.useAsyncState()

return (
<section>
<form style={{display: "flex"}} onSubmit={login}>
<select style={{height: 40}} name="username" defaultValue="1">
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(t => <option value={t}
key={t}>{t}</option>)}
</select>
{/*<input name="password" defaultValue="anypassword"/>*/}
<button disabled={status === "pending"} type="submit">Go</button>
</form>
</section>
)
}
Loading

0 comments on commit 8010b86

Please sign in to comment.