diff --git a/src/landing/.web/bun.lockb b/src/landing/.web/bun.lockb new file mode 100755 index 0000000..55f4e02 Binary files /dev/null and b/src/landing/.web/bun.lockb differ diff --git a/src/landing/.web/env.json b/src/landing/.web/env.json new file mode 100644 index 0000000..e8ba464 --- /dev/null +++ b/src/landing/.web/env.json @@ -0,0 +1 @@ +{"PING": "http://localhost:8000/ping", "EVENT": "ws://localhost:8000/_event", "UPLOAD": "http://localhost:8000/_upload"} \ No newline at end of file diff --git a/src/landing/.web/package.json b/src/landing/.web/package.json index aab86df..139527e 100644 --- a/src/landing/.web/package.json +++ b/src/landing/.web/package.json @@ -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" + "autoprefixer": "10.4.14", + "postcss": "8.4.31", + "tailwindcss": "3.3.2" } } \ No newline at end of file diff --git a/src/landing/.web/pages/404.js b/src/landing/.web/pages/404.js new file mode 100644 index 0000000..20baf35 --- /dev/null +++ b/src/landing/.web/pages/404.js @@ -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 ( + + {isTrue(((!state.is_hydrated) || (connectErrors.length > 0))) ? ( + + + {`wifi_off`} + + +) : ( + +)} + + ) +} + +export function Fragment_14636cc997c0546c0967a25d8e600f96 () { + const [addEvents, connectErrors] = useContext(EventLoopContext); + + + return ( + + {isTrue(connectErrors.length >= 2) ? ( + + = 2}> + + + {`Connection Error`} + + + {`Cannot connect to server: `} + {(connectErrors.length > 0) ? connectErrors[connectErrors.length - 1].message : ''} + {`. Check if server is reachable at `} + {getBackendURL(env.EVENT).href} + + + + +) : ( + +)} + + ) +} + +const pulse = keyframes` + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +` + + +export default function Component() { + + return ( + + +
+ +
+ +
+ + + + + + + + {`About`} + + + + + + + + + {`Projects`} + + + + + + + + + {`Resume`} + + + + + + + + + {`Blog`} + + + + + + + + + {`Shop`} + + + + + + + + + + {`Whoops, this page doesn't exist...`} + + + + + + + + + {`Footer`} + + + + + + + + {`Page Not Found`} + + + + +
+ ) +} diff --git a/src/landing/.web/pages/_app.js b/src/landing/.web/pages/_app.js new file mode 100644 index 0000000..bca9455 --- /dev/null +++ b/src/landing/.web/pages/_app.js @@ -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 ( + + + + {children} + + + + ) +} + +export default function MyApp({ Component, pageProps }) { + return ( + + + + + + + + + + ); +} + diff --git a/src/landing/.web/pages/_document.js b/src/landing/.web/pages/_document.js new file mode 100644 index 0000000..686c734 --- /dev/null +++ b/src/landing/.web/pages/_document.js @@ -0,0 +1,18 @@ +/** @jsxImportSource @emotion/react */ + + +import { Head, Html, Main, NextScript } from "next/document" + + + +export default function Document() { + return ( + + + +
+ + + + ) +} diff --git a/src/landing/.web/pages/index.js b/src/landing/.web/pages/index.js new file mode 100644 index 0000000..e3fcb1a --- /dev/null +++ b/src/landing/.web/pages/index.js @@ -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 ( + + {isTrue(((!state.is_hydrated) || (connectErrors.length > 0))) ? ( + + + {`wifi_off`} + + +) : ( + +)} + + ) +} + +export function Fragment_14636cc997c0546c0967a25d8e600f96 () { + const [addEvents, connectErrors] = useContext(EventLoopContext); + + + return ( + + {isTrue(connectErrors.length >= 2) ? ( + + = 2}> + + + {`Connection Error`} + + + {`Cannot connect to server: `} + {(connectErrors.length > 0) ? connectErrors[connectErrors.length - 1].message : ''} + {`. Check if server is reachable at `} + {getBackendURL(env.EVENT).href} + + + + +) : ( + +)} + + ) +} + +const pulse = keyframes` + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +` + + +export default function Component() { + + return ( + + +
+ +
+ +
+ + + + + + + + {`About`} + + + + + + + + + {`Projects`} + + + + + + + + + {`Resume`} + + + + + + + + + {`Blog`} + + + + + + + + + {`Shop`} + + + + + + + + + + + {`Index`} + + + + + + + + + {`Footer`} + + + + + + + + {`Timothy Pidashev`} + + + + +
+ ) +} diff --git a/src/landing/.web/public/css/scrollbar.css b/src/landing/.web/public/css/scrollbar.css new file mode 100644 index 0000000..1bbdd4a --- /dev/null +++ b/src/landing/.web/public/css/scrollbar.css @@ -0,0 +1,9 @@ +/* Hide scrollbar for all elements */ +::-webkit-scrollbar { + display: none; /* Safari and Chrome */ +} + +/* Hide scrollbar for Firefox */ +html { + scrollbar-width: none; +} diff --git a/src/landing/.web/public/favicon.ico b/src/landing/.web/public/favicon.ico new file mode 100644 index 0000000..166ae99 Binary files /dev/null and b/src/landing/.web/public/favicon.ico differ diff --git a/src/landing/.web/public/fonts/ComicCode-Bold.otf b/src/landing/.web/public/fonts/ComicCode-Bold.otf new file mode 100644 index 0000000..d5d43bf Binary files /dev/null and b/src/landing/.web/public/fonts/ComicCode-Bold.otf differ diff --git a/src/landing/.web/public/fonts/ComicCode-BoldItalic.otf b/src/landing/.web/public/fonts/ComicCode-BoldItalic.otf new file mode 100644 index 0000000..fed9ed8 Binary files /dev/null and b/src/landing/.web/public/fonts/ComicCode-BoldItalic.otf differ diff --git a/src/landing/.web/public/fonts/ComicCode-Italic.otf b/src/landing/.web/public/fonts/ComicCode-Italic.otf new file mode 100644 index 0000000..9bc82a9 Binary files /dev/null and b/src/landing/.web/public/fonts/ComicCode-Italic.otf differ diff --git a/src/landing/.web/public/fonts/ComicCode-Medium.otf b/src/landing/.web/public/fonts/ComicCode-Medium.otf new file mode 100644 index 0000000..dcd6141 Binary files /dev/null and b/src/landing/.web/public/fonts/ComicCode-Medium.otf differ diff --git a/src/landing/.web/public/fonts/ComicCode-MediumItalic.otf b/src/landing/.web/public/fonts/ComicCode-MediumItalic.otf new file mode 100644 index 0000000..3b0866f Binary files /dev/null and b/src/landing/.web/public/fonts/ComicCode-MediumItalic.otf differ diff --git a/src/landing/.web/public/fonts/ComicCode-Regular.otf b/src/landing/.web/public/fonts/ComicCode-Regular.otf new file mode 100644 index 0000000..5f9dd1e Binary files /dev/null and b/src/landing/.web/public/fonts/ComicCode-Regular.otf differ diff --git a/src/landing/.web/public/fonts/ComicCode-Regular.svg b/src/landing/.web/public/fonts/ComicCode-Regular.svg new file mode 100644 index 0000000..9da560f --- /dev/null +++ b/src/landing/.web/public/fonts/ComicCode-Regular.svg @@ -0,0 +1,2937 @@ + + + + +Created by FontForge 20230101 at Wed Mar 17 18:30:11 2021 + By Unknown +Copyright \(c\) 2019 by Toshi Omagari. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/landing/.web/public/fonts/ComicCodeLigatures-Bold.otf b/src/landing/.web/public/fonts/ComicCodeLigatures-Bold.otf new file mode 100644 index 0000000..e58abf0 Binary files /dev/null and b/src/landing/.web/public/fonts/ComicCodeLigatures-Bold.otf differ diff --git a/src/landing/.web/public/fonts/ComicCodeLigatures-BoldItalic.otf b/src/landing/.web/public/fonts/ComicCodeLigatures-BoldItalic.otf new file mode 100644 index 0000000..8156432 Binary files /dev/null and b/src/landing/.web/public/fonts/ComicCodeLigatures-BoldItalic.otf differ diff --git a/src/landing/.web/public/fonts/ComicCodeLigatures-Italic.otf b/src/landing/.web/public/fonts/ComicCodeLigatures-Italic.otf new file mode 100644 index 0000000..d5b3395 Binary files /dev/null and b/src/landing/.web/public/fonts/ComicCodeLigatures-Italic.otf differ diff --git a/src/landing/.web/public/fonts/ComicCodeLigatures-Medium.otf b/src/landing/.web/public/fonts/ComicCodeLigatures-Medium.otf new file mode 100644 index 0000000..c96654f Binary files /dev/null and b/src/landing/.web/public/fonts/ComicCodeLigatures-Medium.otf differ diff --git a/src/landing/.web/public/fonts/ComicCodeLigatures-MediumItalic.otf b/src/landing/.web/public/fonts/ComicCodeLigatures-MediumItalic.otf new file mode 100644 index 0000000..b4748c6 Binary files /dev/null and b/src/landing/.web/public/fonts/ComicCodeLigatures-MediumItalic.otf differ diff --git a/src/landing/.web/public/fonts/ComicCodeLigatures-Regular.otf b/src/landing/.web/public/fonts/ComicCodeLigatures-Regular.otf new file mode 100644 index 0000000..3b57be9 Binary files /dev/null and b/src/landing/.web/public/fonts/ComicCodeLigatures-Regular.otf differ diff --git a/src/landing/.web/public/fonts/fonts.css b/src/landing/.web/public/fonts/fonts.css new file mode 100644 index 0000000..528c706 --- /dev/null +++ b/src/landing/.web/public/fonts/fonts.css @@ -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"); +} + diff --git a/src/landing/.web/reflex.install_frontend_packages.cached b/src/landing/.web/reflex.install_frontend_packages.cached new file mode 100644 index 0000000..702224f --- /dev/null +++ b/src/landing/.web/reflex.install_frontend_packages.cached @@ -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"} \ No newline at end of file diff --git a/src/landing/.web/reflex.json b/src/landing/.web/reflex.json index 5989bd1..a5cbc9d 100644 --- a/src/landing/.web/reflex.json +++ b/src/landing/.web/reflex.json @@ -1 +1 @@ -{"version": "0.4.2", "project_hash": 334487535435764683889748963250527836100} \ No newline at end of file +{"version": "0.4.3", "project_hash": 133404493286750418693195899744043836434} \ No newline at end of file diff --git a/src/landing/.web/styles/styles.css b/src/landing/.web/styles/styles.css new file mode 100644 index 0000000..bc140cf --- /dev/null +++ b/src/landing/.web/styles/styles.css @@ -0,0 +1,3 @@ +@import url('./tailwind.css'); +@import url('@/fonts/fonts.css'); +@import url('@/css/scrollbar.css'); diff --git a/src/landing/.web/tailwind.config.js b/src/landing/.web/tailwind.config.js new file mode 100644 index 0000000..5615a61 --- /dev/null +++ b/src/landing/.web/tailwind.config.js @@ -0,0 +1,7 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./pages/**/*.{js,ts,jsx,tsx}", "./utils/**/*.{js,ts,jsx,tsx}"], + theme: null, + plugins: [ + ], +}; \ No newline at end of file diff --git a/src/landing/.web/utils/components.js b/src/landing/.web/utils/components.js new file mode 100644 index 0000000..a6fae01 --- /dev/null +++ b/src/landing/.web/utils/components.js @@ -0,0 +1,8 @@ +/** @jsxImportSource @emotion/react */ + + +import { memo } from "react" +import { E, isTrue } from "/utils/state" + + + diff --git a/src/landing/.web/utils/context.js b/src/landing/.web/utils/context.js new file mode 100644 index 0000000..f8832e1 --- /dev/null +++ b/src/landing/.web/utils/context.js @@ -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 ( + + {children} + + ) +} + +export function EventLoopProvider({ children }) { + const dispatch = useContext(DispatchContext) + const [addEvents, connectErrors] = useEventLoop( + dispatch, + initialEvents, + clientStorage, + ) + return ( + + {children} + + ) +} + +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 ( + + + + + + + {children} + + + + + + + ) +} \ No newline at end of file diff --git a/src/landing/.web/utils/state.js b/src/landing/.web/utils/state.js index 088f256..51e8c6b 100644 --- a/src/landing/.web/utils/state.js +++ b/src/landing/.web/utils/state.js @@ -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."); } -} +}; diff --git a/src/landing/.web/utils/stateful_components.js b/src/landing/.web/utils/stateful_components.js new file mode 100644 index 0000000..113f7be --- /dev/null +++ b/src/landing/.web/utils/stateful_components.js @@ -0,0 +1,7 @@ +/** @jsxImportSource @emotion/react */ + + + + + + diff --git a/src/landing/.web/utils/theme.js b/src/landing/.web/utils/theme.js new file mode 100644 index 0000000..3631672 --- /dev/null +++ b/src/landing/.web/utils/theme.js @@ -0,0 +1 @@ +export default {"styles": {"global": {":root": {}, "body": {"backgroundColor": "#282828"}}}} \ No newline at end of file diff --git a/src/landing/landing/style.py b/src/landing/landing/style.py index 4cc046f..6ebb947 100644 --- a/src/landing/landing/style.py +++ b/src/landing/landing/style.py @@ -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] }