commit .web, though there's not really a point in tracking this directory

This commit is contained in:
Timothy Pidashev
2024-03-11 08:35:42 -07:00
parent 71b28b6059
commit 47bbbb01fa
33 changed files with 3698 additions and 150 deletions

BIN
src/landing/.web/bun.lockb Executable file

Binary file not shown.

View File

@@ -0,0 +1 @@
{"PING": "http://localhost:8000/ping", "EVENT": "ws://localhost:8000/_event", "UPLOAD": "http://localhost:8000/_upload"}

View File

@@ -8,18 +8,22 @@
}, },
"dependencies": { "dependencies": {
"@emotion/react": "11.11.1", "@emotion/react": "11.11.1",
"@radix-ui/themes": "^2.0.0",
"axios": "1.6.0", "axios": "1.6.0",
"json5": "2.2.3", "json5": "2.2.3",
"lucide-react": "0.314.0",
"next": "14.0.1", "next": "14.0.1",
"next-sitemap": "4.1.8", "next-sitemap": "4.1.8",
"next-themes": "0.2.1", "next-themes": "0.2.1",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"silly": "^0.2.0",
"socket.io-client": "4.6.1", "socket.io-client": "4.6.1",
"universal-cookie": "4.0.4" "universal-cookie": "4.0.4"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"postcss": "8.4.31" "postcss": "8.4.31",
"tailwindcss": "3.3.2"
} }
} }

View File

@@ -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>
)
}

View File

@@ -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>
);
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 407 KiB

View File

@@ -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");
}

View File

@@ -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"}

View File

@@ -1 +1 @@
{"version": "0.4.2", "project_hash": 334487535435764683889748963250527836100} {"version": "0.4.3", "project_hash": 133404493286750418693195899744043836434}

View File

@@ -0,0 +1,3 @@
@import url('./tailwind.css');
@import url('@/fonts/fonts.css');
@import url('@/css/scrollbar.css');

View File

@@ -0,0 +1,7 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./pages/**/*.{js,ts,jsx,tsx}", "./utils/**/*.{js,ts,jsx,tsx}"],
theme: null,
plugins: [
],
};

View File

@@ -0,0 +1,8 @@
/** @jsxImportSource @emotion/react */
import { memo } from "react"
import { E, isTrue } from "/utils/state"

View File

@@ -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>
)
}

View File

@@ -6,14 +6,19 @@ import env from "/env.json";
import Cookies from "universal-cookie"; import Cookies from "universal-cookie";
import { useEffect, useReducer, useRef, useState } from "react"; import { useEffect, useReducer, useRef, useState } from "react";
import Router, { useRouter } from "next/router"; 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. // Endpoint URLs.
const EVENTURL = env.EVENT const EVENTURL = env.EVENT;
const UPLOADURL = env.UPLOAD const UPLOADURL = env.UPLOAD;
// These hostnames indicate that the backend and frontend are reachable via the same domain. // 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. // Global variable to hold the token.
let token; let token;
@@ -28,7 +33,7 @@ const cookies = new Cookies();
export const refs = {}; export const refs = {};
// Flag ensures that only one event is processing on the backend concurrently. // 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. // Array holding pending events to be processed.
const event_queue = []; const event_queue = [];
@@ -64,7 +69,7 @@ export const getToken = () => {
if (token) { if (token) {
return token; return token;
} }
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
if (!window.sessionStorage.getItem(TOKEN_KEY)) { if (!window.sessionStorage.getItem(TOKEN_KEY)) {
window.sessionStorage.setItem(TOKEN_KEY, generateUUID()); window.sessionStorage.setItem(TOKEN_KEY, generateUUID());
} }
@@ -81,7 +86,10 @@ export const getToken = () => {
export const getBackendURL = (url_str) => { export const getBackendURL = (url_str) => {
// Get backend URL object from the endpoint. // Get backend URL object from the endpoint.
const endpoint = new URL(url_str); 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 // Use the frontend domain to access the backend
const frontend_hostname = window.location.hostname; const frontend_hostname = window.location.hostname;
endpoint.hostname = frontend_hostname; endpoint.hostname = frontend_hostname;
@@ -94,8 +102,8 @@ export const getBackendURL = (url_str) => {
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. * Apply a delta to the state.
@@ -103,10 +111,9 @@ export const getBackendURL = (url_str) => {
* @param delta The delta to apply. * @param delta The delta to apply.
*/ */
export const applyDelta = (state, delta) => { export const applyDelta = (state, delta) => {
return { ...state, ...delta } return { ...state, ...delta };
}; };
/** /**
* Handle frontend event or send the event to the backend via Websocket. * Handle frontend event or send the event to the backend via Websocket.
* @param event The event to send. * @param event The event to send.
@@ -117,10 +124,8 @@ export const applyDelta = (state, delta) => {
export const applyEvent = async (event, socket) => { export const applyEvent = async (event, socket) => {
// Handle special events // Handle special events
if (event.name == "_redirect") { if (event.name == "_redirect") {
if (event.payload.external) if (event.payload.external) window.open(event.payload.path, "_blank");
window.open(event.payload.path, "_blank"); else Router.push(event.payload.path);
else
Router.push(event.payload.path);
return false; return false;
} }
@@ -130,20 +135,20 @@ export const applyEvent = async (event, socket) => {
} }
if (event.name == "_remove_cookie") { if (event.name == "_remove_cookie") {
cookies.remove(event.payload.key, { ...event.payload.options }) cookies.remove(event.payload.key, { ...event.payload.options });
queueEvents(initialEvents(), socket) queueEvents(initialEvents(), socket);
return false; return false;
} }
if (event.name == "_clear_local_storage") { if (event.name == "_clear_local_storage") {
localStorage.clear(); localStorage.clear();
queueEvents(initialEvents(), socket) queueEvents(initialEvents(), socket);
return false; return false;
} }
if (event.name == "_remove_local_storage") { if (event.name == "_remove_local_storage") {
localStorage.removeItem(event.payload.key); localStorage.removeItem(event.payload.key);
queueEvents(initialEvents(), socket) queueEvents(initialEvents(), socket);
return false; return false;
} }
@@ -154,9 +159,9 @@ export const applyEvent = async (event, socket) => {
} }
if (event.name == "_download") { if (event.name == "_download") {
const a = document.createElement('a'); const a = document.createElement("a");
a.hidden = true; a.hidden = true;
a.href = event.payload.url a.href = event.payload.url;
a.download = event.payload.filename; a.download = event.payload.filename;
a.click(); a.click();
a.remove(); a.remove();
@@ -178,7 +183,9 @@ export const applyEvent = async (event, socket) => {
if (event.name == "_set_value") { if (event.name == "_set_value") {
const ref = const ref =
event.payload.ref in refs ? refs[event.payload.ref] : event.payload.ref; event.payload.ref in refs ? refs[event.payload.ref] : event.payload.ref;
if (ref.current) {
ref.current.value = event.payload.value; ref.current.value = event.payload.value;
}
return false; return false;
} }
@@ -186,10 +193,10 @@ export const applyEvent = async (event, socket) => {
try { try {
const eval_result = eval(event.payload.javascript_code); const eval_result = eval(event.payload.javascript_code);
if (event.payload.callback) { if (event.payload.callback) {
if (!!eval_result && typeof eval_result.then === 'function') { if (!!eval_result && typeof eval_result.then === "function") {
eval(event.payload.callback)(await eval_result) eval(event.payload.callback)(await eval_result);
} else { } else {
eval(event.payload.callback)(eval_result) eval(event.payload.callback)(eval_result);
} }
} }
} catch (e) { } catch (e) {
@@ -199,14 +206,24 @@ export const applyEvent = async (event, socket) => {
} }
// Update token and router data (if missing). // Update token and router data (if missing).
event.token = getToken() event.token = getToken();
if (event.router_data === undefined || Object.keys(event.router_data).length === 0) { if (
event.router_data = (({ pathname, query, asPath }) => ({ pathname, query, asPath }))(Router) 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. // Send the event to the server.
if (socket) { 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; return true;
} }
@@ -242,17 +259,15 @@ export const applyRestEvent = async (event, socket) => {
* @param socket The socket object to send the event on. * @param socket The socket object to send the event on.
*/ */
export const queueEvents = async (events, socket) => { export const queueEvents = async (events, socket) => {
event_queue.push(...events) event_queue.push(...events);
await processEvent(socket.current) await processEvent(socket.current);
} };
/** /**
* Process an event off the event queue. * Process an event off the event queue.
* @param socket The socket object to send the event on. * @param socket The socket object to send the event on.
*/ */
export const processEvent = async ( export const processEvent = async (socket) => {
socket
) => {
// Only proceed if the socket is up, otherwise we throw the event into the void // Only proceed if the socket is up, otherwise we throw the event into the void
if (!socket) { if (!socket) {
return; return;
@@ -264,12 +279,12 @@ export const processEvent = async (
} }
// Set processing to true to block other events from being processed. // Set processing to true to block other events from being processed.
event_processing = true event_processing = true;
// Apply the next event in the queue. // Apply the next event in the queue.
const event = event_queue.shift(); const event = event_queue.shift();
let eventSent = false let eventSent = false;
// Process events with handlers via REST and all others via websockets. // Process events with handlers via REST and all others via websockets.
if (event.handler) { if (event.handler) {
eventSent = await applyRestEvent(event, socket); eventSent = await applyRestEvent(event, socket);
@@ -281,27 +296,27 @@ export const processEvent = async (
event_processing = false; event_processing = false;
// recursively call processEvent to drain the queue, since there is // recursively call processEvent to drain the queue, since there is
// no state update to trigger the useEffect event loop. // no state update to trigger the useEffect event loop.
await processEvent(socket) await processEvent(socket);
}
} }
};
/** /**
* Connect to a websocket and set the handlers. * Connect to a websocket and set the handlers.
* @param socket The socket object to connect. * @param socket The socket object to connect.
* @param dispatch The function to queue state update * @param dispatch The function to queue state update
* @param transports The transports to use. * @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 * @param client_storage The client storage object from context.js
*/ */
export const connect = async ( export const connect = async (
socket, socket,
dispatch, dispatch,
transports, transports,
setConnectError, setConnectErrors,
client_storage = {}, client_storage = {}
) => { ) => {
// Get backend URL object from the endpoint. // Get backend URL object from the endpoint.
const endpoint = getBackendURL(EVENTURL) const endpoint = getBackendURL(EVENTURL);
// Create the socket. // Create the socket.
socket.current = io(endpoint.href, { socket.current = io(endpoint.href, {
@@ -310,27 +325,39 @@ export const connect = async (
autoUnref: false, 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. // Once the socket is open, hydrate the page.
socket.current.on("connect", () => { socket.current.on("connect", () => {
setConnectError(null) setConnectErrors([]);
}); });
socket.current.on('connect_error', (error) => { socket.current.on("connect_error", (error) => {
setConnectError(error) setConnectErrors((connectErrors) => [connectErrors.slice(-9), error]);
}); });
// On each received message, queue the updates and events. // On each received message, queue the updates and events.
socket.current.on("event", message => { socket.current.on("event", (message) => {
const update = JSON5.parse(message) const update = JSON5.parse(message);
for (const substate in update.delta) { for (const substate in update.delta) {
dispatch[substate](update.delta[substate]) dispatch[substate](update.delta[substate]);
} }
applyClientStorageDelta(client_storage, update.delta) applyClientStorageDelta(client_storage, update.delta);
event_processing = !update.final event_processing = !update.final;
if (update.events) { 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. * @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 // return if there's no file to upload
if (files === undefined || files.length === 0) { if (files === undefined || files.length === 0) {
return false; return false;
} }
if (upload_controllers[upload_id]) { 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; return false;
} }
let resp_idx = 0; let resp_idx = 0;
const eventHandler = (progressEvent) => { const eventHandler = (progressEvent) => {
// handle any delta / event streamed from the upload event handler // 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) => { chunks.slice(resp_idx).map((chunk) => {
try { try {
socket._callbacks.$event.map((f) => { socket._callbacks.$event.map((f) => {
f(chunk) f(chunk);
}) });
resp_idx += 1 resp_idx += 1;
} catch (e) { } catch (e) {
if (progressEvent.progress === 1) { if (progressEvent.progress === 1) {
// Chunk may be incomplete, so only report errors when full response is available. // 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 = { const config = {
headers: { headers: {
"Reflex-Client-Token": getToken(), "Reflex-Client-Token": getToken(),
@@ -383,26 +416,22 @@ export const uploadFiles = async (handler, files, upload_id, on_upload_progress,
}, },
signal: controller.signal, signal: controller.signal,
onDownloadProgress: eventHandler, onDownloadProgress: eventHandler,
} };
if (on_upload_progress) { if (on_upload_progress) {
config["onUploadProgress"] = on_upload_progress config["onUploadProgress"] = on_upload_progress;
} }
const formdata = new FormData(); const formdata = new FormData();
// Add the token and handler to the file name. // Add the token and handler to the file name.
files.forEach((file) => { files.forEach((file) => {
formdata.append( formdata.append("files", file, file.path || file.name);
"files", });
file,
file.path || file.name
);
})
// Send the file to the server. // Send the file to the server.
upload_controllers[upload_id] = controller upload_controllers[upload_id] = controller;
try { try {
return await axios.post(getBackendURL(UPLOADURL), formdata, config) return await axios.post(getBackendURL(UPLOADURL), formdata, config);
} catch (error) { } catch (error) {
if (error.response) { if (error.response) {
// The request was made and the server responded with a status code // 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; return false;
} finally { } 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 * @returns payload dict of client storage values
*/ */
export const hydrateClientStorage = (client_storage) => { export const hydrateClientStorage = (client_storage) => {
const client_storage_values = {} const client_storage_values = {};
if (client_storage.cookies) { if (client_storage.cookies) {
for (const state_key in client_storage.cookies) { for (const state_key in client_storage.cookies) {
const cookie_options = client_storage.cookies[state_key] const cookie_options = client_storage.cookies[state_key];
const cookie_name = cookie_options.name || state_key const cookie_name = cookie_options.name || state_key;
const cookie_value = cookies.get(cookie_name) const cookie_value = cookies.get(cookie_name);
if (cookie_value !== undefined) { 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) { for (const state_key in client_storage.local_storage) {
const options = client_storage.local_storage[state_key] const options = client_storage.local_storage[state_key];
const local_storage_value = localStorage.getItem(options.name || state_key) const local_storage_value = localStorage.getItem(
options.name || state_key
);
if (local_storage_value !== null) { 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) { 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) => { const applyClientStorageDelta = (client_storage, delta) => {
// find the main state and check for is_hydrated // 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) { 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) { if (main_state.is_hydrated !== undefined && !main_state.is_hydrated) {
// skip if the state is not hydrated yet, since all client storage // skip if the state is not hydrated yet, since all client storage
// values are sent in the hydrate event // 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. // Save known client storage values to cookies and localStorage.
for (const substate in delta) { for (const substate in delta) {
for (const key in delta[substate]) { 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) { if (client_storage.cookies && state_key in client_storage.cookies) {
const cookie_options = { ...client_storage.cookies[state_key] } const cookie_options = { ...client_storage.cookies[state_key] };
const cookie_name = cookie_options.name || state_key const cookie_name = cookie_options.name || state_key;
delete cookie_options.name // name is not a valid cookie option delete cookie_options.name; // name is not a valid cookie option
cookies.set(cookie_name, delta[substate][key], cookie_options); 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')) { } else if (
const options = client_storage.local_storage[state_key] 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]); localStorage.setItem(options.name || state_key, delta[substate][key]);
} }
} }
} }
} };
/** /**
* Establish websocket event loop for a NextJS page. * 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 initial_events The initial app events.
* @param client_storage The client storage object from context.js * @param client_storage The client storage object from context.js
* *
* @returns [addEvents, connectError] - * @returns [addEvents, connectErrors] -
* addEvents is used to queue an event, and * 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 = ( export const useEventLoop = (
dispatch, dispatch,
initial_events = () => [], initial_events = () => [],
client_storage = {}, client_storage = {}
) => { ) => {
const socket = useRef(null) const socket = useRef(null);
const router = useRouter() const router = useRouter();
const [connectError, setConnectError] = useState(null) const [connectErrors, setConnectErrors] = useState([]);
// Function to add new events to the event queue. // Function to add new events to the event queue.
const addEvents = (events, _e, event_actions) => { const addEvents = (events, _e, event_actions) => {
@@ -527,22 +564,26 @@ export const useEventLoop = (
if (event_actions?.stopPropagation && _e?.stopPropagation) { if (event_actions?.stopPropagation && _e?.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(() => { useEffect(() => {
if (router.isReady && !sentHydrate.current) { if (router.isReady && !sentHydrate.current) {
const events = initial_events() const events = initial_events();
addEvents(events.map((e) => ( addEvents(
{ events.map((e) => ({
...e, ...e,
router_data: (({ pathname, query, asPath }) => ({ pathname, query, asPath }))(router) router_data: (({ pathname, query, asPath }) => ({
pathname,
query,
asPath,
}))(router),
}))
);
sentHydrate.current = true;
} }
))) }, [router.isReady]);
sentHydrate.current = true
}
}, [router.isReady])
// Main event loop. // Main event loop.
useEffect(() => { useEffect(() => {
@@ -554,17 +595,22 @@ export const useEventLoop = (
if (Object.keys(initialState).length > 1) { if (Object.keys(initialState).length > 1) {
// Initialize the websocket connection. // Initialize the websocket connection.
if (!socket.current) { if (!socket.current) {
connect(socket, dispatch, ['websocket', 'polling'], setConnectError, client_storage) connect(
socket,
dispatch,
["websocket", "polling"],
setConnectErrors,
client_storage
);
} }
(async () => { (async () => {
// Process all outstanding events. // Process all outstanding events.
while (event_queue.length > 0 && !event_processing) { while (event_queue.length > 0 && !event_processing) {
await processEvent(socket.current) await processEvent(socket.current);
} }
})() })();
} }
}) });
// localStorage event handling // localStorage event handling
useEffect(() => { useEffect(() => {
@@ -583,9 +629,12 @@ export const useEventLoop = (
// e is StorageEvent // e is StorageEvent
const handleStorage = (e) => { const handleStorage = (e) => {
if (storage_to_state_map[e.key]) { if (storage_to_state_map[e.key]) {
const vars = {} const vars = {};
vars[storage_to_state_map[e.key]] = e.newValue vars[storage_to_state_map[e.key]] = e.newValue;
const event = Event(`${state_name}.update_vars_internal`, {vars: vars}) const event = Event(
`${state_name}.update_vars_internal_state.update_vars_internal`,
{ vars: vars }
);
addEvents([event], e); addEvents([event], e);
} }
}; };
@@ -594,18 +643,17 @@ export const useEventLoop = (
return () => window.removeEventListener("storage", handleStorage); return () => window.removeEventListener("storage", handleStorage);
}); });
// Route after the initial page hydration. // Route after the initial page hydration.
useEffect(() => { useEffect(() => {
const change_complete = () => addEvents(onLoadInternalEvent()) const change_complete = () => addEvents(onLoadInternalEvent());
router.events.on('routeChangeComplete', change_complete) router.events.on("routeChangeComplete", change_complete);
return () => { return () => {
router.events.off('routeChangeComplete', change_complete) router.events.off("routeChangeComplete", change_complete);
} };
}, [router]) }, [router]);
return [addEvents, connectError] return [addEvents, connectErrors];
} };
/*** /***
* Check if a value is truthy in python. * Check if a value is truthy in python.
@@ -627,16 +675,24 @@ export const getRefValue = (ref) => {
} }
if (ref.current.type == "checkbox") { if (ref.current.type == "checkbox") {
return ref.current.checked; // chakra return ref.current.checked; // chakra
} else if (ref.current.className.includes("rt-CheckboxButton") || ref.current.className.includes("rt-SwitchButton")) { } else if (
ref.current.className?.includes("rt-CheckboxButton") ||
ref.current.className?.includes("rt-SwitchButton")
) {
return ref.current.ariaChecked == "true"; // radix return ref.current.ariaChecked == "true"; // radix
} else if (ref.current.className.includes("rt-SliderRoot")) { } else if (ref.current.className?.includes("rt-SliderRoot")) {
// find the actual slider // find the actual slider
return ref.current.querySelector(".rt-SliderThumb").ariaValueNow; return ref.current.querySelector(".rt-SliderThumb")?.ariaValueNow;
} else { } else {
//querySelector(":checked") is needed to get value from radio_group //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. * Get the values from a ref array.
@@ -648,8 +704,12 @@ export const getRefValues = (refs) => {
return; return;
} }
// getAttribute is used by RangeSlider because it doesn't assign value // 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. * Spread two arrays or two objects.
@@ -660,9 +720,9 @@ export const getRefValues = (refs) => {
export const spreadArraysOrObjects = (first, second) => { export const spreadArraysOrObjects = (first, second) => {
if (Array.isArray(first) && Array.isArray(second)) { if (Array.isArray(first) && Array.isArray(second)) {
return [...first, ...second]; return [...first, ...second];
} else if (typeof first === 'object' && typeof second === 'object') { } else if (typeof first === "object" && typeof second === "object") {
return { ...first, ...second }; return { ...first, ...second };
} else { } else {
throw new Error('Both parameters must be either arrays or objects.'); throw new Error("Both parameters must be either arrays or objects.");
}
} }
};

View File

@@ -0,0 +1,7 @@
/** @jsxImportSource @emotion/react */

View File

@@ -0,0 +1 @@
export default {"styles": {"global": {":root": {}, "body": {"backgroundColor": "#282828"}}}}

View File

@@ -53,7 +53,7 @@ base_style = {
"font_family": "ComicCode", "font_family": "ComicCode",
"font_size": 24, "font_size": 24,
"color": color["black"], "color": color["black"],
"text_decoration": "underline", "text_decoration": "none",
"_hover": { "_hover": {
"color": color["green"][100] "color": color["green"][100]
} }