commit .web, though there's not really a point in tracking this directory
This commit is contained in:
Executable
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"PING": "http://localhost:8000/ping", "EVENT": "ws://localhost:8000/_event", "UPLOAD": "http://localhost:8000/_upload"}
|
||||
@@ -8,18 +8,22 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.11.1",
|
||||
"@radix-ui/themes": "^2.0.0",
|
||||
"axios": "1.6.0",
|
||||
"json5": "2.2.3",
|
||||
"lucide-react": "0.314.0",
|
||||
"next": "14.0.1",
|
||||
"next-sitemap": "4.1.8",
|
||||
"next-themes": "0.2.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"silly": "^0.2.0",
|
||||
"socket.io-client": "4.6.1",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "10.4.14",
|
||||
"postcss": "8.4.31"
|
||||
"postcss": "8.4.31",
|
||||
"tailwindcss": "3.3.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/** @jsxImportSource @emotion/react */
|
||||
|
||||
|
||||
import { Fragment, useContext } from "react"
|
||||
import { EventLoopContext, StateContexts } from "/utils/context"
|
||||
import { Event, getBackendURL, isTrue } from "/utils/state"
|
||||
import { WifiOffIcon as LucideWifiOffIcon } from "lucide-react"
|
||||
import { keyframes } from "@emotion/react"
|
||||
import { Box as RadixThemesBox, Dialog as RadixThemesDialog, Flex as RadixThemesFlex, Heading as RadixThemesHeading, Link as RadixThemesLink, Text as RadixThemesText } from "@radix-ui/themes"
|
||||
import env from "/env.json"
|
||||
import NextLink from "next/link"
|
||||
import NextHead from "next/head"
|
||||
|
||||
|
||||
|
||||
export function Fragment_966c0378eb9d65bdfb5286644be9b831 () {
|
||||
const [addEvents, connectErrors] = useContext(EventLoopContext);
|
||||
const state = useContext(StateContexts.state)
|
||||
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{isTrue(((!state.is_hydrated) || (connectErrors.length > 0))) ? (
|
||||
<Fragment>
|
||||
<LucideWifiOffIcon css={{"color": "crimson", "zIndex": 9999, "position": "fixed", "bottom": "30px", "right": "30px", "animation": `${pulse} 1s infinite`}} size={32}>
|
||||
{`wifi_off`}
|
||||
</LucideWifiOffIcon>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment/>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export function Fragment_14636cc997c0546c0967a25d8e600f96 () {
|
||||
const [addEvents, connectErrors] = useContext(EventLoopContext);
|
||||
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{isTrue(connectErrors.length >= 2) ? (
|
||||
<Fragment>
|
||||
<RadixThemesDialog.Root css={{"zIndex": 9999}} open={connectErrors.length >= 2}>
|
||||
<RadixThemesDialog.Content>
|
||||
<RadixThemesDialog.Title>
|
||||
{`Connection Error`}
|
||||
</RadixThemesDialog.Title>
|
||||
<RadixThemesText as={`p`} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#ebdbb2"}}>
|
||||
{`Cannot connect to server: `}
|
||||
{(connectErrors.length > 0) ? connectErrors[connectErrors.length - 1].message : ''}
|
||||
{`. Check if server is reachable at `}
|
||||
{getBackendURL(env.EVENT).href}
|
||||
</RadixThemesText>
|
||||
</RadixThemesDialog.Content>
|
||||
</RadixThemesDialog.Root>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment/>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
const pulse = keyframes`
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
export default function Component() {
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Fragment>
|
||||
<div css={{"position": "fixed", "width": "100vw", "height": "0"}}>
|
||||
<Fragment_966c0378eb9d65bdfb5286644be9b831/>
|
||||
</div>
|
||||
<Fragment_14636cc997c0546c0967a25d8e600f96/>
|
||||
</Fragment>
|
||||
<RadixThemesBox>
|
||||
<RadixThemesBox>
|
||||
<RadixThemesFlex css={{"display": "flex", "alignItems": "center", "justifyContent": "center"}} gap={`7`}>
|
||||
<RadixThemesFlex>
|
||||
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
|
||||
<NextLink href={`http://about.timmypidashev.localhost`} passHref={true}>
|
||||
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
|
||||
{`About`}
|
||||
</RadixThemesText>
|
||||
</NextLink>
|
||||
</RadixThemesLink>
|
||||
</RadixThemesFlex>
|
||||
<RadixThemesFlex>
|
||||
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
|
||||
<NextLink href={`http://projects.timmypidashev.localhost`} passHref={true}>
|
||||
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
|
||||
{`Projects`}
|
||||
</RadixThemesText>
|
||||
</NextLink>
|
||||
</RadixThemesLink>
|
||||
</RadixThemesFlex>
|
||||
<RadixThemesFlex>
|
||||
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
|
||||
<NextLink href={`http://resume.timmypidashev.localhost`} passHref={true}>
|
||||
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
|
||||
{`Resume`}
|
||||
</RadixThemesText>
|
||||
</NextLink>
|
||||
</RadixThemesLink>
|
||||
</RadixThemesFlex>
|
||||
<RadixThemesFlex>
|
||||
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
|
||||
<NextLink href={`http://blog.timmypidashev.localhost`} passHref={true}>
|
||||
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
|
||||
{`Blog`}
|
||||
</RadixThemesText>
|
||||
</NextLink>
|
||||
</RadixThemesLink>
|
||||
</RadixThemesFlex>
|
||||
<RadixThemesFlex>
|
||||
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
|
||||
<NextLink href={`http://shop.timmypidashev.localhost`} passHref={true}>
|
||||
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
|
||||
{`Shop`}
|
||||
</RadixThemesText>
|
||||
</NextLink>
|
||||
</RadixThemesLink>
|
||||
</RadixThemesFlex>
|
||||
</RadixThemesFlex>
|
||||
</RadixThemesBox>
|
||||
<RadixThemesFlex css={{"height": "100vh", "width": "100%", "display": "flex", "alignItems": "center", "justifyContent": "center"}}>
|
||||
<RadixThemesFlex align={`start`} direction={`column`} gap={`2`}>
|
||||
<RadixThemesHeading css={{"fontFamily": "ComicCode", "fontSize": 32, "color": "#ebdbb2"}} size={`9`}>
|
||||
{`Whoops, this page doesn't exist...`}
|
||||
</RadixThemesHeading>
|
||||
<RadixThemesFlex css={{"flex": 1, "justifySelf": "stretch", "alignSelf": "stretch"}}/>
|
||||
</RadixThemesFlex>
|
||||
</RadixThemesFlex>
|
||||
<RadixThemesBox css={{"borderTop": "2px solid #ebdbb2;"}}>
|
||||
<RadixThemesFlex css={{"height": "15vh", "display": "flex", "alignItems": "center", "justifyContent": "center"}}>
|
||||
<RadixThemesFlex align={`center`} direction={`column`} gap={`7`}>
|
||||
<RadixThemesHeading css={{"fontFamily": "ComicCode", "fontSize": 32, "color": "#ebdbb2"}} size={`9`}>
|
||||
{`Footer`}
|
||||
</RadixThemesHeading>
|
||||
</RadixThemesFlex>
|
||||
</RadixThemesFlex>
|
||||
</RadixThemesBox>
|
||||
</RadixThemesBox>
|
||||
<NextHead>
|
||||
<title>
|
||||
{`Page Not Found`}
|
||||
</title>
|
||||
<meta content={`A Reflex app.`} name={`description`}/>
|
||||
<meta content={`favicon.ico`} property={`og:image`}/>
|
||||
</NextHead>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/** @jsxImportSource @emotion/react */
|
||||
|
||||
import '/styles/styles.css'
|
||||
|
||||
import RadixThemesColorModeProvider from "/components/reflex/radix_themes_color_mode_provider.js"
|
||||
import { Theme as RadixThemesTheme } from "@radix-ui/themes"
|
||||
import "@radix-ui/themes/styles.css"
|
||||
import theme from "/utils/theme.js"
|
||||
import { Fragment } from "react"
|
||||
|
||||
|
||||
import { EventLoopProvider, StateProvider, defaultColorMode } from "/utils/context.js";
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
|
||||
|
||||
|
||||
function AppWrap({children}) {
|
||||
|
||||
|
||||
return (
|
||||
<RadixThemesColorModeProvider>
|
||||
<RadixThemesTheme accentColor={`blue`} css={{...theme.styles.global[':root'], ...theme.styles.global.body}}>
|
||||
<Fragment>
|
||||
{children}
|
||||
</Fragment>
|
||||
</RadixThemesTheme>
|
||||
</RadixThemesColorModeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
<ThemeProvider defaultTheme={ defaultColorMode } storageKey="chakra-ui-color-mode" attribute="class">
|
||||
<AppWrap>
|
||||
<StateProvider>
|
||||
<EventLoopProvider>
|
||||
<Component {...pageProps} />
|
||||
</EventLoopProvider>
|
||||
</StateProvider>
|
||||
</AppWrap>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/** @jsxImportSource @emotion/react */
|
||||
|
||||
|
||||
import { Head, Html, Main, NextScript } from "next/document"
|
||||
|
||||
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html>
|
||||
<Head/>
|
||||
<body>
|
||||
<Main/>
|
||||
<NextScript/>
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/** @jsxImportSource @emotion/react */
|
||||
|
||||
|
||||
import { Fragment, useContext } from "react"
|
||||
import { EventLoopContext, StateContexts } from "/utils/context"
|
||||
import { Event, getBackendURL, isTrue } from "/utils/state"
|
||||
import { WifiOffIcon as LucideWifiOffIcon } from "lucide-react"
|
||||
import { keyframes } from "@emotion/react"
|
||||
import { Box as RadixThemesBox, Dialog as RadixThemesDialog, Flex as RadixThemesFlex, Heading as RadixThemesHeading, Link as RadixThemesLink, Text as RadixThemesText } from "@radix-ui/themes"
|
||||
import env from "/env.json"
|
||||
import NextLink from "next/link"
|
||||
import NextHead from "next/head"
|
||||
|
||||
|
||||
|
||||
export function Fragment_966c0378eb9d65bdfb5286644be9b831 () {
|
||||
const [addEvents, connectErrors] = useContext(EventLoopContext);
|
||||
const state = useContext(StateContexts.state)
|
||||
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{isTrue(((!state.is_hydrated) || (connectErrors.length > 0))) ? (
|
||||
<Fragment>
|
||||
<LucideWifiOffIcon css={{"color": "crimson", "zIndex": 9999, "position": "fixed", "bottom": "30px", "right": "30px", "animation": `${pulse} 1s infinite`}} size={32}>
|
||||
{`wifi_off`}
|
||||
</LucideWifiOffIcon>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment/>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export function Fragment_14636cc997c0546c0967a25d8e600f96 () {
|
||||
const [addEvents, connectErrors] = useContext(EventLoopContext);
|
||||
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{isTrue(connectErrors.length >= 2) ? (
|
||||
<Fragment>
|
||||
<RadixThemesDialog.Root css={{"zIndex": 9999}} open={connectErrors.length >= 2}>
|
||||
<RadixThemesDialog.Content>
|
||||
<RadixThemesDialog.Title>
|
||||
{`Connection Error`}
|
||||
</RadixThemesDialog.Title>
|
||||
<RadixThemesText as={`p`} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#ebdbb2"}}>
|
||||
{`Cannot connect to server: `}
|
||||
{(connectErrors.length > 0) ? connectErrors[connectErrors.length - 1].message : ''}
|
||||
{`. Check if server is reachable at `}
|
||||
{getBackendURL(env.EVENT).href}
|
||||
</RadixThemesText>
|
||||
</RadixThemesDialog.Content>
|
||||
</RadixThemesDialog.Root>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment/>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
const pulse = keyframes`
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
export default function Component() {
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Fragment>
|
||||
<div css={{"position": "fixed", "width": "100vw", "height": "0"}}>
|
||||
<Fragment_966c0378eb9d65bdfb5286644be9b831/>
|
||||
</div>
|
||||
<Fragment_14636cc997c0546c0967a25d8e600f96/>
|
||||
</Fragment>
|
||||
<RadixThemesBox>
|
||||
<RadixThemesBox>
|
||||
<RadixThemesFlex css={{"display": "flex", "alignItems": "center", "justifyContent": "center"}} gap={`7`}>
|
||||
<RadixThemesFlex>
|
||||
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
|
||||
<NextLink href={`http://about.timmypidashev.localhost`} passHref={true}>
|
||||
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
|
||||
{`About`}
|
||||
</RadixThemesText>
|
||||
</NextLink>
|
||||
</RadixThemesLink>
|
||||
</RadixThemesFlex>
|
||||
<RadixThemesFlex>
|
||||
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
|
||||
<NextLink href={`http://projects.timmypidashev.localhost`} passHref={true}>
|
||||
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
|
||||
{`Projects`}
|
||||
</RadixThemesText>
|
||||
</NextLink>
|
||||
</RadixThemesLink>
|
||||
</RadixThemesFlex>
|
||||
<RadixThemesFlex>
|
||||
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
|
||||
<NextLink href={`http://resume.timmypidashev.localhost`} passHref={true}>
|
||||
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
|
||||
{`Resume`}
|
||||
</RadixThemesText>
|
||||
</NextLink>
|
||||
</RadixThemesLink>
|
||||
</RadixThemesFlex>
|
||||
<RadixThemesFlex>
|
||||
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
|
||||
<NextLink href={`http://blog.timmypidashev.localhost`} passHref={true}>
|
||||
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
|
||||
{`Blog`}
|
||||
</RadixThemesText>
|
||||
</NextLink>
|
||||
</RadixThemesLink>
|
||||
</RadixThemesFlex>
|
||||
<RadixThemesFlex>
|
||||
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
|
||||
<NextLink href={`http://shop.timmypidashev.localhost`} passHref={true}>
|
||||
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
|
||||
{`Shop`}
|
||||
</RadixThemesText>
|
||||
</NextLink>
|
||||
</RadixThemesLink>
|
||||
</RadixThemesFlex>
|
||||
</RadixThemesFlex>
|
||||
</RadixThemesBox>
|
||||
<RadixThemesBox>
|
||||
<RadixThemesFlex css={{"height": "100vh", "display": "flex", "alignItems": "center", "justifyContent": "center"}}>
|
||||
<RadixThemesFlex align={`center`} direction={`column`} gap={`7`}>
|
||||
<RadixThemesHeading css={{"fontFamily": "ComicCode", "fontSize": 32, "color": "#ebdbb2"}} size={`9`}>
|
||||
{`Index`}
|
||||
</RadixThemesHeading>
|
||||
</RadixThemesFlex>
|
||||
</RadixThemesFlex>
|
||||
</RadixThemesBox>
|
||||
<RadixThemesBox css={{"borderTop": "2px solid #ebdbb2;"}}>
|
||||
<RadixThemesFlex css={{"height": "15vh", "display": "flex", "alignItems": "center", "justifyContent": "center"}}>
|
||||
<RadixThemesFlex align={`center`} direction={`column`} gap={`7`}>
|
||||
<RadixThemesHeading css={{"fontFamily": "ComicCode", "fontSize": 32, "color": "#ebdbb2"}} size={`9`}>
|
||||
{`Footer`}
|
||||
</RadixThemesHeading>
|
||||
</RadixThemesFlex>
|
||||
</RadixThemesFlex>
|
||||
</RadixThemesBox>
|
||||
</RadixThemesBox>
|
||||
<NextHead>
|
||||
<title>
|
||||
{`Timothy Pidashev`}
|
||||
</title>
|
||||
<meta content={`A Reflex app.`} name={`description`}/>
|
||||
<meta content={`favicon.ico`} property={`og:image`}/>
|
||||
</NextHead>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/* Hide scrollbar for all elements */
|
||||
::-webkit-scrollbar {
|
||||
display: none; /* Safari and Chrome */
|
||||
}
|
||||
|
||||
/* Hide scrollbar for Firefox */
|
||||
html {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 407 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
@font-face {
|
||||
font-family: ComicCode;
|
||||
src: url("ComicCode-Regular.otf") format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ComicCodeBold;
|
||||
font-weight: bold;
|
||||
src: url("ComicCode-Bold.otf") format("opentype");
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
['@radix-ui/themes@^2.0.0', 'lucide-react@0.314.0'],{"app_name": "landing", "loglevel": "info", "frontend_port": 3000, "frontend_path": "", "backend_port": 8000, "api_url": "http://localhost:8000", "deploy_url": "http://localhost:3000", "backend_host": "0.0.0.0", "db_url": "sqlite:///reflex.db", "redis_url": null, "telemetry_enabled": true, "bun_path": "/home/timmy/.local/share/reflex/bun/bin/bun", "cors_allowed_origins": ["*"], "tailwind": {"content": ["./pages/**/*.{js,ts,jsx,tsx}", "./utils/**/*.{js,ts,jsx,tsx}"]}, "timeout": 120, "next_compression": true, "event_namespace": null, "frontend_packages": [], "cp_backend_url": "https://rxcp-prod-control-plane.fly.dev", "cp_web_url": "https://control-plane.reflex.run", "gunicorn_worker_class": "uvicorn.workers.UvicornH11Worker"}
|
||||
@@ -1 +1 @@
|
||||
{"version": "0.4.2", "project_hash": 334487535435764683889748963250527836100}
|
||||
{"version": "0.4.3", "project_hash": 133404493286750418693195899744043836434}
|
||||
@@ -0,0 +1,3 @@
|
||||
@import url('./tailwind.css');
|
||||
@import url('@/fonts/fonts.css');
|
||||
@import url('@/css/scrollbar.css');
|
||||
@@ -0,0 +1,7 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./pages/**/*.{js,ts,jsx,tsx}", "./utils/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: null,
|
||||
plugins: [
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @jsxImportSource @emotion/react */
|
||||
|
||||
|
||||
import { memo } from "react"
|
||||
import { E, isTrue } from "/utils/state"
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { createContext, useContext, useMemo, useReducer, useState } from "react"
|
||||
import { applyDelta, Event, hydrateClientStorage, useEventLoop, refs } from "/utils/state.js"
|
||||
|
||||
export const initialState = {"state": {"is_hydrated": false, "router": {"session": {"client_token": "", "client_ip": "", "session_id": ""}, "headers": {"host": "", "origin": "", "upgrade": "", "connection": "", "pragma": "", "cache_control": "", "user_agent": "", "sec_websocket_version": "", "sec_websocket_key": "", "sec_websocket_extensions": "", "accept_encoding": "", "accept_language": ""}, "page": {"host": "", "path": "", "raw_path": "", "full_path": "", "full_raw_path": "", "params": {}}}}, "state.on_load_internal_state": {}, "state.update_vars_internal_state": {}, "state.state": {}, "state.state.theme_state": {"current_theme": 0, "theme": {"background_color": "#282828"}, "themes": {"0": {"background_color": "#282828"}, "1": {"background_color": "#000000"}}}}
|
||||
|
||||
export const defaultColorMode = "light"
|
||||
export const ColorModeContext = createContext(null);
|
||||
export const UploadFilesContext = createContext(null);
|
||||
export const DispatchContext = createContext(null);
|
||||
export const StateContexts = {
|
||||
state: createContext(null),
|
||||
state__on_load_internal_state: createContext(null),
|
||||
state__update_vars_internal_state: createContext(null),
|
||||
state__state: createContext(null),
|
||||
state__state__theme_state: createContext(null),
|
||||
}
|
||||
export const EventLoopContext = createContext(null);
|
||||
export const clientStorage = {"cookies": {}, "local_storage": {}}
|
||||
|
||||
export const state_name = "state"
|
||||
|
||||
// Theses events are triggered on initial load and each page navigation.
|
||||
export const onLoadInternalEvent = () => {
|
||||
const internal_events = [];
|
||||
|
||||
// Get tracked cookie and local storage vars to send to the backend.
|
||||
const client_storage_vars = hydrateClientStorage(clientStorage);
|
||||
// But only send the vars if any are actually set in the browser.
|
||||
if (client_storage_vars && Object.keys(client_storage_vars).length !== 0) {
|
||||
internal_events.push(
|
||||
Event(
|
||||
'state.update_vars_internal_state.update_vars_internal',
|
||||
{vars: client_storage_vars},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// `on_load_internal` triggers the correct on_load event(s) for the current page.
|
||||
// If the page does not define any on_load event, this will just set `is_hydrated = true`.
|
||||
internal_events.push(Event('state.on_load_internal_state.on_load_internal'));
|
||||
|
||||
return internal_events;
|
||||
}
|
||||
|
||||
// The following events are sent when the websocket connects or reconnects.
|
||||
export const initialEvents = () => [
|
||||
Event('state.hydrate'),
|
||||
...onLoadInternalEvent()
|
||||
]
|
||||
|
||||
export const isDevMode = true
|
||||
|
||||
export function UploadFilesProvider({ children }) {
|
||||
const [filesById, setFilesById] = useState({})
|
||||
refs["__clear_selected_files"] = (id) => setFilesById(filesById => {
|
||||
const newFilesById = {...filesById}
|
||||
delete newFilesById[id]
|
||||
return newFilesById
|
||||
})
|
||||
return (
|
||||
<UploadFilesContext.Provider value={[filesById, setFilesById]}>
|
||||
{children}
|
||||
</UploadFilesContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function EventLoopProvider({ children }) {
|
||||
const dispatch = useContext(DispatchContext)
|
||||
const [addEvents, connectErrors] = useEventLoop(
|
||||
dispatch,
|
||||
initialEvents,
|
||||
clientStorage,
|
||||
)
|
||||
return (
|
||||
<EventLoopContext.Provider value={[addEvents, connectErrors]}>
|
||||
{children}
|
||||
</EventLoopContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function StateProvider({ children }) {
|
||||
const [state, dispatch_state] = useReducer(applyDelta, initialState["state"])
|
||||
const [state__on_load_internal_state, dispatch_state__on_load_internal_state] = useReducer(applyDelta, initialState["state.on_load_internal_state"])
|
||||
const [state__update_vars_internal_state, dispatch_state__update_vars_internal_state] = useReducer(applyDelta, initialState["state.update_vars_internal_state"])
|
||||
const [state__state, dispatch_state__state] = useReducer(applyDelta, initialState["state.state"])
|
||||
const [state__state__theme_state, dispatch_state__state__theme_state] = useReducer(applyDelta, initialState["state.state.theme_state"])
|
||||
const dispatchers = useMemo(() => {
|
||||
return {
|
||||
"state": dispatch_state,
|
||||
"state.on_load_internal_state": dispatch_state__on_load_internal_state,
|
||||
"state.update_vars_internal_state": dispatch_state__update_vars_internal_state,
|
||||
"state.state": dispatch_state__state,
|
||||
"state.state.theme_state": dispatch_state__state__theme_state,
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<StateContexts.state.Provider value={ state }>
|
||||
<StateContexts.state__on_load_internal_state.Provider value={ state__on_load_internal_state }>
|
||||
<StateContexts.state__update_vars_internal_state.Provider value={ state__update_vars_internal_state }>
|
||||
<StateContexts.state__state.Provider value={ state__state }>
|
||||
<StateContexts.state__state__theme_state.Provider value={ state__state__theme_state }>
|
||||
<DispatchContext.Provider value={dispatchers}>
|
||||
{children}
|
||||
</DispatchContext.Provider>
|
||||
</StateContexts.state__state__theme_state.Provider>
|
||||
</StateContexts.state__state.Provider>
|
||||
</StateContexts.state__update_vars_internal_state.Provider>
|
||||
</StateContexts.state__on_load_internal_state.Provider>
|
||||
</StateContexts.state.Provider>
|
||||
)
|
||||
}
|
||||
+206
-146
@@ -6,14 +6,19 @@ import env from "/env.json";
|
||||
import Cookies from "universal-cookie";
|
||||
import { useEffect, useReducer, useRef, useState } from "react";
|
||||
import Router, { useRouter } from "next/router";
|
||||
import { initialEvents, initialState, onLoadInternalEvent, state_name } from "utils/context.js"
|
||||
import {
|
||||
initialEvents,
|
||||
initialState,
|
||||
onLoadInternalEvent,
|
||||
state_name,
|
||||
} from "utils/context.js";
|
||||
|
||||
// Endpoint URLs.
|
||||
const EVENTURL = env.EVENT
|
||||
const UPLOADURL = env.UPLOAD
|
||||
const EVENTURL = env.EVENT;
|
||||
const UPLOADURL = env.UPLOAD;
|
||||
|
||||
// These hostnames indicate that the backend and frontend are reachable via the same domain.
|
||||
const SAME_DOMAIN_HOSTNAMES = ["localhost", "0.0.0.0", "::", "0:0:0:0:0:0:0:0"]
|
||||
const SAME_DOMAIN_HOSTNAMES = ["localhost", "0.0.0.0", "::", "0:0:0:0:0:0:0:0"];
|
||||
|
||||
// Global variable to hold the token.
|
||||
let token;
|
||||
@@ -28,7 +33,7 @@ const cookies = new Cookies();
|
||||
export const refs = {};
|
||||
|
||||
// Flag ensures that only one event is processing on the backend concurrently.
|
||||
let event_processing = false
|
||||
let event_processing = false;
|
||||
// Array holding pending events to be processed.
|
||||
const event_queue = [];
|
||||
|
||||
@@ -64,7 +69,7 @@ export const getToken = () => {
|
||||
if (token) {
|
||||
return token;
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof window !== "undefined") {
|
||||
if (!window.sessionStorage.getItem(TOKEN_KEY)) {
|
||||
window.sessionStorage.setItem(TOKEN_KEY, generateUUID());
|
||||
}
|
||||
@@ -81,7 +86,10 @@ export const getToken = () => {
|
||||
export const getBackendURL = (url_str) => {
|
||||
// Get backend URL object from the endpoint.
|
||||
const endpoint = new URL(url_str);
|
||||
if ((typeof window !== 'undefined') && SAME_DOMAIN_HOSTNAMES.includes(endpoint.hostname)) {
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
SAME_DOMAIN_HOSTNAMES.includes(endpoint.hostname)
|
||||
) {
|
||||
// Use the frontend domain to access the backend
|
||||
const frontend_hostname = window.location.hostname;
|
||||
endpoint.hostname = frontend_hostname;
|
||||
@@ -91,11 +99,11 @@ export const getBackendURL = (url_str) => {
|
||||
} else if (endpoint.protocol === "http:") {
|
||||
endpoint.protocol = "https:";
|
||||
}
|
||||
endpoint.port = ""; // Assume websocket is on https port via load balancer.
|
||||
endpoint.port = ""; // Assume websocket is on https port via load balancer.
|
||||
}
|
||||
}
|
||||
return endpoint
|
||||
}
|
||||
return endpoint;
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply a delta to the state.
|
||||
@@ -103,10 +111,9 @@ export const getBackendURL = (url_str) => {
|
||||
* @param delta The delta to apply.
|
||||
*/
|
||||
export const applyDelta = (state, delta) => {
|
||||
return { ...state, ...delta }
|
||||
return { ...state, ...delta };
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handle frontend event or send the event to the backend via Websocket.
|
||||
* @param event The event to send.
|
||||
@@ -117,10 +124,8 @@ export const applyDelta = (state, delta) => {
|
||||
export const applyEvent = async (event, socket) => {
|
||||
// Handle special events
|
||||
if (event.name == "_redirect") {
|
||||
if (event.payload.external)
|
||||
window.open(event.payload.path, "_blank");
|
||||
else
|
||||
Router.push(event.payload.path);
|
||||
if (event.payload.external) window.open(event.payload.path, "_blank");
|
||||
else Router.push(event.payload.path);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -130,20 +135,20 @@ export const applyEvent = async (event, socket) => {
|
||||
}
|
||||
|
||||
if (event.name == "_remove_cookie") {
|
||||
cookies.remove(event.payload.key, { ...event.payload.options })
|
||||
queueEvents(initialEvents(), socket)
|
||||
cookies.remove(event.payload.key, { ...event.payload.options });
|
||||
queueEvents(initialEvents(), socket);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.name == "_clear_local_storage") {
|
||||
localStorage.clear();
|
||||
queueEvents(initialEvents(), socket)
|
||||
queueEvents(initialEvents(), socket);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.name == "_remove_local_storage") {
|
||||
localStorage.removeItem(event.payload.key);
|
||||
queueEvents(initialEvents(), socket)
|
||||
queueEvents(initialEvents(), socket);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -154,9 +159,9 @@ export const applyEvent = async (event, socket) => {
|
||||
}
|
||||
|
||||
if (event.name == "_download") {
|
||||
const a = document.createElement('a');
|
||||
const a = document.createElement("a");
|
||||
a.hidden = true;
|
||||
a.href = event.payload.url
|
||||
a.href = event.payload.url;
|
||||
a.download = event.payload.filename;
|
||||
a.click();
|
||||
a.remove();
|
||||
@@ -178,7 +183,9 @@ export const applyEvent = async (event, socket) => {
|
||||
if (event.name == "_set_value") {
|
||||
const ref =
|
||||
event.payload.ref in refs ? refs[event.payload.ref] : event.payload.ref;
|
||||
ref.current.value = event.payload.value;
|
||||
if (ref.current) {
|
||||
ref.current.value = event.payload.value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -186,10 +193,10 @@ export const applyEvent = async (event, socket) => {
|
||||
try {
|
||||
const eval_result = eval(event.payload.javascript_code);
|
||||
if (event.payload.callback) {
|
||||
if (!!eval_result && typeof eval_result.then === 'function') {
|
||||
eval(event.payload.callback)(await eval_result)
|
||||
if (!!eval_result && typeof eval_result.then === "function") {
|
||||
eval(event.payload.callback)(await eval_result);
|
||||
} else {
|
||||
eval(event.payload.callback)(eval_result)
|
||||
eval(event.payload.callback)(eval_result);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -199,14 +206,24 @@ export const applyEvent = async (event, socket) => {
|
||||
}
|
||||
|
||||
// Update token and router data (if missing).
|
||||
event.token = getToken()
|
||||
if (event.router_data === undefined || Object.keys(event.router_data).length === 0) {
|
||||
event.router_data = (({ pathname, query, asPath }) => ({ pathname, query, asPath }))(Router)
|
||||
event.token = getToken();
|
||||
if (
|
||||
event.router_data === undefined ||
|
||||
Object.keys(event.router_data).length === 0
|
||||
) {
|
||||
event.router_data = (({ pathname, query, asPath }) => ({
|
||||
pathname,
|
||||
query,
|
||||
asPath,
|
||||
}))(Router);
|
||||
}
|
||||
|
||||
// Send the event to the server.
|
||||
if (socket) {
|
||||
socket.emit("event", JSON.stringify(event, (k, v) => v === undefined ? null : v));
|
||||
socket.emit(
|
||||
"event",
|
||||
JSON.stringify(event, (k, v) => (v === undefined ? null : v))
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -242,17 +259,15 @@ export const applyRestEvent = async (event, socket) => {
|
||||
* @param socket The socket object to send the event on.
|
||||
*/
|
||||
export const queueEvents = async (events, socket) => {
|
||||
event_queue.push(...events)
|
||||
await processEvent(socket.current)
|
||||
}
|
||||
event_queue.push(...events);
|
||||
await processEvent(socket.current);
|
||||
};
|
||||
|
||||
/**
|
||||
* Process an event off the event queue.
|
||||
* @param socket The socket object to send the event on.
|
||||
*/
|
||||
export const processEvent = async (
|
||||
socket
|
||||
) => {
|
||||
export const processEvent = async (socket) => {
|
||||
// Only proceed if the socket is up, otherwise we throw the event into the void
|
||||
if (!socket) {
|
||||
return;
|
||||
@@ -264,12 +279,12 @@ export const processEvent = async (
|
||||
}
|
||||
|
||||
// Set processing to true to block other events from being processed.
|
||||
event_processing = true
|
||||
event_processing = true;
|
||||
|
||||
// Apply the next event in the queue.
|
||||
const event = event_queue.shift();
|
||||
|
||||
let eventSent = false
|
||||
let eventSent = false;
|
||||
// Process events with handlers via REST and all others via websockets.
|
||||
if (event.handler) {
|
||||
eventSent = await applyRestEvent(event, socket);
|
||||
@@ -281,27 +296,27 @@ export const processEvent = async (
|
||||
event_processing = false;
|
||||
// recursively call processEvent to drain the queue, since there is
|
||||
// no state update to trigger the useEffect event loop.
|
||||
await processEvent(socket)
|
||||
await processEvent(socket);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Connect to a websocket and set the handlers.
|
||||
* @param socket The socket object to connect.
|
||||
* @param dispatch The function to queue state update
|
||||
* @param transports The transports to use.
|
||||
* @param setConnectError The function to update connection error value.
|
||||
* @param setConnectErrors The function to update connection error value.
|
||||
* @param client_storage The client storage object from context.js
|
||||
*/
|
||||
export const connect = async (
|
||||
socket,
|
||||
dispatch,
|
||||
transports,
|
||||
setConnectError,
|
||||
client_storage = {},
|
||||
setConnectErrors,
|
||||
client_storage = {}
|
||||
) => {
|
||||
// Get backend URL object from the endpoint.
|
||||
const endpoint = getBackendURL(EVENTURL)
|
||||
const endpoint = getBackendURL(EVENTURL);
|
||||
|
||||
// Create the socket.
|
||||
socket.current = io(endpoint.href, {
|
||||
@@ -310,27 +325,39 @@ export const connect = async (
|
||||
autoUnref: false,
|
||||
});
|
||||
|
||||
function checkVisibility() {
|
||||
if (document.visibilityState === "visible") {
|
||||
if (!socket.current.connected) {
|
||||
console.log("Socket is disconnected, attempting to reconnect ");
|
||||
socket.current.connect();
|
||||
} else {
|
||||
console.log("Socket is reconnected ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Once the socket is open, hydrate the page.
|
||||
socket.current.on("connect", () => {
|
||||
setConnectError(null)
|
||||
setConnectErrors([]);
|
||||
});
|
||||
|
||||
socket.current.on('connect_error', (error) => {
|
||||
setConnectError(error)
|
||||
socket.current.on("connect_error", (error) => {
|
||||
setConnectErrors((connectErrors) => [connectErrors.slice(-9), error]);
|
||||
});
|
||||
|
||||
// On each received message, queue the updates and events.
|
||||
socket.current.on("event", message => {
|
||||
const update = JSON5.parse(message)
|
||||
socket.current.on("event", (message) => {
|
||||
const update = JSON5.parse(message);
|
||||
for (const substate in update.delta) {
|
||||
dispatch[substate](update.delta[substate])
|
||||
dispatch[substate](update.delta[substate]);
|
||||
}
|
||||
applyClientStorageDelta(client_storage, update.delta)
|
||||
event_processing = !update.final
|
||||
applyClientStorageDelta(client_storage, update.delta);
|
||||
event_processing = !update.final;
|
||||
if (update.events) {
|
||||
queueEvents(update.events, socket)
|
||||
queueEvents(update.events, socket);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", checkVisibility);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -344,38 +371,44 @@ export const connect = async (
|
||||
*
|
||||
* @returns The response from posting to the UPLOADURL endpoint.
|
||||
*/
|
||||
export const uploadFiles = async (handler, files, upload_id, on_upload_progress, socket) => {
|
||||
export const uploadFiles = async (
|
||||
handler,
|
||||
files,
|
||||
upload_id,
|
||||
on_upload_progress,
|
||||
socket
|
||||
) => {
|
||||
// return if there's no file to upload
|
||||
if (files === undefined || files.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (upload_controllers[upload_id]) {
|
||||
console.log("Upload already in progress for ", upload_id)
|
||||
console.log("Upload already in progress for ", upload_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
let resp_idx = 0;
|
||||
const eventHandler = (progressEvent) => {
|
||||
// handle any delta / event streamed from the upload event handler
|
||||
const chunks = progressEvent.event.target.responseText.trim().split("\n")
|
||||
const chunks = progressEvent.event.target.responseText.trim().split("\n");
|
||||
chunks.slice(resp_idx).map((chunk) => {
|
||||
try {
|
||||
socket._callbacks.$event.map((f) => {
|
||||
f(chunk)
|
||||
})
|
||||
resp_idx += 1
|
||||
f(chunk);
|
||||
});
|
||||
resp_idx += 1;
|
||||
} catch (e) {
|
||||
if (progressEvent.progress === 1) {
|
||||
// Chunk may be incomplete, so only report errors when full response is available.
|
||||
console.log("Error parsing chunk", chunk, e)
|
||||
console.log("Error parsing chunk", chunk, e);
|
||||
}
|
||||
return
|
||||
return;
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const controller = new AbortController()
|
||||
const controller = new AbortController();
|
||||
const config = {
|
||||
headers: {
|
||||
"Reflex-Client-Token": getToken(),
|
||||
@@ -383,26 +416,22 @@ export const uploadFiles = async (handler, files, upload_id, on_upload_progress,
|
||||
},
|
||||
signal: controller.signal,
|
||||
onDownloadProgress: eventHandler,
|
||||
}
|
||||
};
|
||||
if (on_upload_progress) {
|
||||
config["onUploadProgress"] = on_upload_progress
|
||||
config["onUploadProgress"] = on_upload_progress;
|
||||
}
|
||||
const formdata = new FormData();
|
||||
|
||||
// Add the token and handler to the file name.
|
||||
files.forEach((file) => {
|
||||
formdata.append(
|
||||
"files",
|
||||
file,
|
||||
file.path || file.name
|
||||
);
|
||||
})
|
||||
formdata.append("files", file, file.path || file.name);
|
||||
});
|
||||
|
||||
// Send the file to the server.
|
||||
upload_controllers[upload_id] = controller
|
||||
upload_controllers[upload_id] = controller;
|
||||
|
||||
try {
|
||||
return await axios.post(getBackendURL(UPLOADURL), formdata, config)
|
||||
return await axios.post(getBackendURL(UPLOADURL), formdata, config);
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
@@ -419,7 +448,7 @@ export const uploadFiles = async (handler, files, upload_id, on_upload_progress,
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
delete upload_controllers[upload_id]
|
||||
delete upload_controllers[upload_id];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -441,30 +470,32 @@ export const Event = (name, payload = {}, handler = null) => {
|
||||
* @returns payload dict of client storage values
|
||||
*/
|
||||
export const hydrateClientStorage = (client_storage) => {
|
||||
const client_storage_values = {}
|
||||
const client_storage_values = {};
|
||||
if (client_storage.cookies) {
|
||||
for (const state_key in client_storage.cookies) {
|
||||
const cookie_options = client_storage.cookies[state_key]
|
||||
const cookie_name = cookie_options.name || state_key
|
||||
const cookie_value = cookies.get(cookie_name)
|
||||
const cookie_options = client_storage.cookies[state_key];
|
||||
const cookie_name = cookie_options.name || state_key;
|
||||
const cookie_value = cookies.get(cookie_name);
|
||||
if (cookie_value !== undefined) {
|
||||
client_storage_values[state_key] = cookies.get(cookie_name)
|
||||
client_storage_values[state_key] = cookies.get(cookie_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (client_storage.local_storage && (typeof window !== 'undefined')) {
|
||||
if (client_storage.local_storage && typeof window !== "undefined") {
|
||||
for (const state_key in client_storage.local_storage) {
|
||||
const options = client_storage.local_storage[state_key]
|
||||
const local_storage_value = localStorage.getItem(options.name || state_key)
|
||||
const options = client_storage.local_storage[state_key];
|
||||
const local_storage_value = localStorage.getItem(
|
||||
options.name || state_key
|
||||
);
|
||||
if (local_storage_value !== null) {
|
||||
client_storage_values[state_key] = local_storage_value
|
||||
client_storage_values[state_key] = local_storage_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (client_storage.cookies || client_storage.local_storage) {
|
||||
return client_storage_values
|
||||
return client_storage_values;
|
||||
}
|
||||
return {}
|
||||
return {};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -474,9 +505,11 @@ export const hydrateClientStorage = (client_storage) => {
|
||||
*/
|
||||
const applyClientStorageDelta = (client_storage, delta) => {
|
||||
// find the main state and check for is_hydrated
|
||||
const unqualified_states = Object.keys(delta).filter((key) => key.split(".").length === 1);
|
||||
const unqualified_states = Object.keys(delta).filter(
|
||||
(key) => key.split(".").length === 1
|
||||
);
|
||||
if (unqualified_states.length === 1) {
|
||||
const main_state = delta[unqualified_states[0]]
|
||||
const main_state = delta[unqualified_states[0]];
|
||||
if (main_state.is_hydrated !== undefined && !main_state.is_hydrated) {
|
||||
// skip if the state is not hydrated yet, since all client storage
|
||||
// values are sent in the hydrate event
|
||||
@@ -486,19 +519,23 @@ const applyClientStorageDelta = (client_storage, delta) => {
|
||||
// Save known client storage values to cookies and localStorage.
|
||||
for (const substate in delta) {
|
||||
for (const key in delta[substate]) {
|
||||
const state_key = `${substate}.${key}`
|
||||
const state_key = `${substate}.${key}`;
|
||||
if (client_storage.cookies && state_key in client_storage.cookies) {
|
||||
const cookie_options = { ...client_storage.cookies[state_key] }
|
||||
const cookie_name = cookie_options.name || state_key
|
||||
delete cookie_options.name // name is not a valid cookie option
|
||||
const cookie_options = { ...client_storage.cookies[state_key] };
|
||||
const cookie_name = cookie_options.name || state_key;
|
||||
delete cookie_options.name; // name is not a valid cookie option
|
||||
cookies.set(cookie_name, delta[substate][key], cookie_options);
|
||||
} else if (client_storage.local_storage && state_key in client_storage.local_storage && (typeof window !== 'undefined')) {
|
||||
const options = client_storage.local_storage[state_key]
|
||||
} else if (
|
||||
client_storage.local_storage &&
|
||||
state_key in client_storage.local_storage &&
|
||||
typeof window !== "undefined"
|
||||
) {
|
||||
const options = client_storage.local_storage[state_key];
|
||||
localStorage.setItem(options.name || state_key, delta[substate][key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Establish websocket event loop for a NextJS page.
|
||||
@@ -506,18 +543,18 @@ const applyClientStorageDelta = (client_storage, delta) => {
|
||||
* @param initial_events The initial app events.
|
||||
* @param client_storage The client storage object from context.js
|
||||
*
|
||||
* @returns [addEvents, connectError] -
|
||||
* @returns [addEvents, connectErrors] -
|
||||
* addEvents is used to queue an event, and
|
||||
* connectError is a reactive js error from the websocket connection (or null if connected).
|
||||
* connectErrors is an array of reactive js error from the websocket connection (or null if connected).
|
||||
*/
|
||||
export const useEventLoop = (
|
||||
dispatch,
|
||||
initial_events = () => [],
|
||||
client_storage = {},
|
||||
client_storage = {}
|
||||
) => {
|
||||
const socket = useRef(null)
|
||||
const router = useRouter()
|
||||
const [connectError, setConnectError] = useState(null)
|
||||
const socket = useRef(null);
|
||||
const router = useRouter();
|
||||
const [connectErrors, setConnectErrors] = useState([]);
|
||||
|
||||
// Function to add new events to the event queue.
|
||||
const addEvents = (events, _e, event_actions) => {
|
||||
@@ -527,22 +564,26 @@ export const useEventLoop = (
|
||||
if (event_actions?.stopPropagation && _e?.stopPropagation) {
|
||||
_e.stopPropagation();
|
||||
}
|
||||
queueEvents(events, socket)
|
||||
}
|
||||
queueEvents(events, socket);
|
||||
};
|
||||
|
||||
const sentHydrate = useRef(false); // Avoid double-hydrate due to React strict-mode
|
||||
const sentHydrate = useRef(false); // Avoid double-hydrate due to React strict-mode
|
||||
useEffect(() => {
|
||||
if (router.isReady && !sentHydrate.current) {
|
||||
const events = initial_events()
|
||||
addEvents(events.map((e) => (
|
||||
{
|
||||
const events = initial_events();
|
||||
addEvents(
|
||||
events.map((e) => ({
|
||||
...e,
|
||||
router_data: (({ pathname, query, asPath }) => ({ pathname, query, asPath }))(router)
|
||||
}
|
||||
)))
|
||||
sentHydrate.current = true
|
||||
router_data: (({ pathname, query, asPath }) => ({
|
||||
pathname,
|
||||
query,
|
||||
asPath,
|
||||
}))(router),
|
||||
}))
|
||||
);
|
||||
sentHydrate.current = true;
|
||||
}
|
||||
}, [router.isReady])
|
||||
}, [router.isReady]);
|
||||
|
||||
// Main event loop.
|
||||
useEffect(() => {
|
||||
@@ -554,17 +595,22 @@ export const useEventLoop = (
|
||||
if (Object.keys(initialState).length > 1) {
|
||||
// Initialize the websocket connection.
|
||||
if (!socket.current) {
|
||||
connect(socket, dispatch, ['websocket', 'polling'], setConnectError, client_storage)
|
||||
connect(
|
||||
socket,
|
||||
dispatch,
|
||||
["websocket", "polling"],
|
||||
setConnectErrors,
|
||||
client_storage
|
||||
);
|
||||
}
|
||||
(async () => {
|
||||
// Process all outstanding events.
|
||||
while (event_queue.length > 0 && !event_processing) {
|
||||
await processEvent(socket.current)
|
||||
await processEvent(socket.current);
|
||||
}
|
||||
})()
|
||||
})();
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
// localStorage event handling
|
||||
useEffect(() => {
|
||||
@@ -583,9 +629,12 @@ export const useEventLoop = (
|
||||
// e is StorageEvent
|
||||
const handleStorage = (e) => {
|
||||
if (storage_to_state_map[e.key]) {
|
||||
const vars = {}
|
||||
vars[storage_to_state_map[e.key]] = e.newValue
|
||||
const event = Event(`${state_name}.update_vars_internal`, {vars: vars})
|
||||
const vars = {};
|
||||
vars[storage_to_state_map[e.key]] = e.newValue;
|
||||
const event = Event(
|
||||
`${state_name}.update_vars_internal_state.update_vars_internal`,
|
||||
{ vars: vars }
|
||||
);
|
||||
addEvents([event], e);
|
||||
}
|
||||
};
|
||||
@@ -594,18 +643,17 @@ export const useEventLoop = (
|
||||
return () => window.removeEventListener("storage", handleStorage);
|
||||
});
|
||||
|
||||
|
||||
// Route after the initial page hydration.
|
||||
useEffect(() => {
|
||||
const change_complete = () => addEvents(onLoadInternalEvent())
|
||||
router.events.on('routeChangeComplete', change_complete)
|
||||
const change_complete = () => addEvents(onLoadInternalEvent());
|
||||
router.events.on("routeChangeComplete", change_complete);
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', change_complete)
|
||||
}
|
||||
}, [router])
|
||||
router.events.off("routeChangeComplete", change_complete);
|
||||
};
|
||||
}, [router]);
|
||||
|
||||
return [addEvents, connectError]
|
||||
}
|
||||
return [addEvents, connectErrors];
|
||||
};
|
||||
|
||||
/***
|
||||
* Check if a value is truthy in python.
|
||||
@@ -626,17 +674,25 @@ export const getRefValue = (ref) => {
|
||||
return;
|
||||
}
|
||||
if (ref.current.type == "checkbox") {
|
||||
return ref.current.checked; // chakra
|
||||
} else if (ref.current.className.includes("rt-CheckboxButton") || ref.current.className.includes("rt-SwitchButton")) {
|
||||
return ref.current.ariaChecked == "true"; // radix
|
||||
} else if (ref.current.className.includes("rt-SliderRoot")) {
|
||||
return ref.current.checked; // chakra
|
||||
} else if (
|
||||
ref.current.className?.includes("rt-CheckboxButton") ||
|
||||
ref.current.className?.includes("rt-SwitchButton")
|
||||
) {
|
||||
return ref.current.ariaChecked == "true"; // radix
|
||||
} else if (ref.current.className?.includes("rt-SliderRoot")) {
|
||||
// find the actual slider
|
||||
return ref.current.querySelector(".rt-SliderThumb").ariaValueNow;
|
||||
return ref.current.querySelector(".rt-SliderThumb")?.ariaValueNow;
|
||||
} else {
|
||||
//querySelector(":checked") is needed to get value from radio_group
|
||||
return ref.current.value || (ref.current.querySelector(':checked') && ref.current.querySelector(':checked').value);
|
||||
return (
|
||||
ref.current.value ||
|
||||
(ref.current.querySelector &&
|
||||
ref.current.querySelector(":checked") &&
|
||||
ref.current.querySelector(":checked")?.value)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the values from a ref array.
|
||||
@@ -648,21 +704,25 @@ export const getRefValues = (refs) => {
|
||||
return;
|
||||
}
|
||||
// getAttribute is used by RangeSlider because it doesn't assign value
|
||||
return refs.map((ref) => ref.current ? ref.current.value || ref.current.getAttribute("aria-valuenow") : null);
|
||||
}
|
||||
return refs.map((ref) =>
|
||||
ref.current
|
||||
? ref.current.value || ref.current.getAttribute("aria-valuenow")
|
||||
: null
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Spread two arrays or two objects.
|
||||
* @param first The first array or object.
|
||||
* @param second The second array or object.
|
||||
* @returns The final merged array or object.
|
||||
*/
|
||||
* Spread two arrays or two objects.
|
||||
* @param first The first array or object.
|
||||
* @param second The second array or object.
|
||||
* @returns The final merged array or object.
|
||||
*/
|
||||
export const spreadArraysOrObjects = (first, second) => {
|
||||
if (Array.isArray(first) && Array.isArray(second)) {
|
||||
return [...first, ...second];
|
||||
} else if (typeof first === 'object' && typeof second === 'object') {
|
||||
} else if (typeof first === "object" && typeof second === "object") {
|
||||
return { ...first, ...second };
|
||||
} else {
|
||||
throw new Error('Both parameters must be either arrays or objects.');
|
||||
throw new Error("Both parameters must be either arrays or objects.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/** @jsxImportSource @emotion/react */
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export default {"styles": {"global": {":root": {}, "body": {"backgroundColor": "#282828"}}}}
|
||||
@@ -53,7 +53,7 @@ base_style = {
|
||||
"font_family": "ComicCode",
|
||||
"font_size": 24,
|
||||
"color": color["black"],
|
||||
"text_decoration": "underline",
|
||||
"text_decoration": "none",
|
||||
"_hover": {
|
||||
"color": color["green"][100]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user