diff --git a/src/landing/.gitignore b/src/landing/.gitignore
deleted file mode 100644
index eab0d4b..0000000
--- a/src/landing/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-*.db
-*.py[cod]
-.web
-__pycache__/
\ No newline at end of file
diff --git a/src/landing/.web/.gitignore b/src/landing/.web/.gitignore
new file mode 100644
index 0000000..534bc86
--- /dev/null
+++ b/src/landing/.web/.gitignore
@@ -0,0 +1,39 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+/_static
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# vercel
+.vercel
+
+# DS_Store
+.DS_Store
\ No newline at end of file
diff --git a/src/landing/.web/components/reflex/chakra_color_mode_provider.js b/src/landing/.web/components/reflex/chakra_color_mode_provider.js
new file mode 100644
index 0000000..f897522
--- /dev/null
+++ b/src/landing/.web/components/reflex/chakra_color_mode_provider.js
@@ -0,0 +1,21 @@
+import { useColorMode as chakraUseColorMode } from "@chakra-ui/react"
+import { useTheme } from "next-themes"
+import { useEffect } from "react"
+import { ColorModeContext } from "/utils/context.js"
+
+export default function ChakraColorModeProvider({ children }) {
+ const {colorMode, toggleColorMode} = chakraUseColorMode()
+ const {theme, setTheme} = useTheme()
+
+ useEffect(() => {
+ if (colorMode != theme) {
+ toggleColorMode()
+ }
+ }, [theme])
+
+ return (
+
+ {children}
+
+ )
+}
\ No newline at end of file
diff --git a/src/landing/.web/components/reflex/radix_themes_color_mode_provider.js b/src/landing/.web/components/reflex/radix_themes_color_mode_provider.js
new file mode 100644
index 0000000..e2dd563
--- /dev/null
+++ b/src/landing/.web/components/reflex/radix_themes_color_mode_provider.js
@@ -0,0 +1,22 @@
+import { useTheme } from "next-themes"
+import { useEffect, useState } from "react"
+import { ColorModeContext, defaultColorMode } from "/utils/context.js"
+
+
+export default function RadixThemesColorModeProvider({ children }) {
+ const {theme, setTheme} = useTheme()
+ const [colorMode, setColorMode] = useState(defaultColorMode)
+
+ useEffect(() => {
+ setColorMode(theme)
+ }, [theme])
+
+ const toggleColorMode = () => {
+ setTheme(theme === "light" ? "dark" : "light")
+ }
+ return (
+
+ {children}
+
+ )
+ }
\ No newline at end of file
diff --git a/src/landing/.web/jsconfig.json b/src/landing/.web/jsconfig.json
new file mode 100644
index 0000000..3c8a325
--- /dev/null
+++ b/src/landing/.web/jsconfig.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["public/*"]
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/landing/.web/next.config.js b/src/landing/.web/next.config.js
new file mode 100644
index 0000000..42dad1d
--- /dev/null
+++ b/src/landing/.web/next.config.js
@@ -0,0 +1 @@
+module.exports = {basePath: "", compress: true, reactStrictMode: true, trailingSlash: true};
diff --git a/src/landing/.web/package.json b/src/landing/.web/package.json
new file mode 100644
index 0000000..aab86df
--- /dev/null
+++ b/src/landing/.web/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "reflex",
+ "scripts": {
+ "dev": "next dev",
+ "export": "next build",
+ "export-sitemap": "next build && next-sitemap",
+ "prod": "next start"
+ },
+ "dependencies": {
+ "@emotion/react": "11.11.1",
+ "axios": "1.6.0",
+ "json5": "2.2.3",
+ "next": "14.0.1",
+ "next-sitemap": "4.1.8",
+ "next-themes": "0.2.1",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "socket.io-client": "4.6.1",
+ "universal-cookie": "4.0.4"
+ },
+ "devDependencies": {
+ "autoprefixer": "10.4.14",
+ "postcss": "8.4.31"
+ }
+}
\ No newline at end of file
diff --git a/src/landing/.web/postcss.config.js b/src/landing/.web/postcss.config.js
new file mode 100644
index 0000000..33ad091
--- /dev/null
+++ b/src/landing/.web/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/src/landing/.web/reflex.json b/src/landing/.web/reflex.json
new file mode 100644
index 0000000..5989bd1
--- /dev/null
+++ b/src/landing/.web/reflex.json
@@ -0,0 +1 @@
+{"version": "0.4.2", "project_hash": 334487535435764683889748963250527836100}
\ No newline at end of file
diff --git a/src/landing/.web/styles/tailwind.css b/src/landing/.web/styles/tailwind.css
new file mode 100644
index 0000000..b5c61c9
--- /dev/null
+++ b/src/landing/.web/styles/tailwind.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/src/landing/.web/utils/client_side_routing.js b/src/landing/.web/utils/client_side_routing.js
new file mode 100644
index 0000000..75fb581
--- /dev/null
+++ b/src/landing/.web/utils/client_side_routing.js
@@ -0,0 +1,36 @@
+import { useEffect, useRef, useState } from "react";
+import { useRouter } from "next/router";
+
+/**
+ * React hook for use in /404 page to enable client-side routing.
+ *
+ * Uses the next/router to redirect to the provided URL when loading
+ * the 404 page (for example as a fallback in static hosting situations).
+ *
+ * @returns {boolean} routeNotFound - true if the current route is an actual 404
+ */
+export const useClientSideRouting = () => {
+ const [routeNotFound, setRouteNotFound] = useState(false)
+ const didRedirect = useRef(false)
+ const router = useRouter()
+ useEffect(() => {
+ if (
+ router.isReady &&
+ !didRedirect.current // have not tried redirecting yet
+ ) {
+ didRedirect.current = true // never redirect twice to avoid "Hard Navigate" error
+ // attempt to redirect to the route in the browser address bar once
+ router.replace({
+ pathname: window.location.pathname,
+ query: window.location.search.slice(1),
+ })
+ .catch((e) => {
+ setRouteNotFound(true) // navigation failed, so this is a real 404
+ })
+ }
+ }, [router.isReady]);
+
+ // Return the reactive bool, to avoid flashing 404 page until we know for sure
+ // the route is not found.
+ return routeNotFound
+}
\ No newline at end of file
diff --git a/src/landing/.web/utils/helpers/dataeditor.js b/src/landing/.web/utils/helpers/dataeditor.js
new file mode 100644
index 0000000..9ff3682
--- /dev/null
+++ b/src/landing/.web/utils/helpers/dataeditor.js
@@ -0,0 +1,69 @@
+import { GridCellKind } from "@glideapps/glide-data-grid";
+
+export function getDEColumn(columns, col) {
+ let c = columns[col];
+ c.pos = col;
+ return c;
+}
+
+export function getDERow(data, row) {
+ return data[row];
+}
+
+export function locateCell(row, column) {
+ if (Array.isArray(row)) {
+ return row[column.pos];
+ } else {
+ return row[column.id];
+ }
+}
+
+export function formatCell(value, column) {
+ const editable = column.editable ?? true;
+ switch (column.type) {
+ case "int":
+ case "float":
+ return {
+ kind: GridCellKind.Number,
+ data: value,
+ displayData: value + "",
+ readonly: !editable,
+ allowOverlay: editable,
+ };
+ case "datetime":
+ // value = moment format?
+ case "str":
+ return {
+ kind: GridCellKind.Text,
+ data: value,
+ displayData: value,
+ readonly: !editable,
+ allowOverlay: editable,
+ };
+ case "bool":
+ return {
+ kind: GridCellKind.Boolean,
+ data: value,
+ readonly: !editable,
+ };
+ default:
+ console.log(
+ "Warning: column.type is undefined for column.title=" + column.title
+ );
+ return {
+ kind: GridCellKind.Text,
+ data: value,
+ displayData: column.type,
+ };
+ }
+}
+
+export function formatDataEditorCells(col, row, columns, data) {
+ if (row < data.length && col < columns.length) {
+ const column = getDEColumn(columns, col);
+ const rowData = getDERow(data, row);
+ const cellData = locateCell(rowData, column);
+ return formatCell(cellData, column);
+ }
+ return { kind: GridCellKind.Loading };
+}
diff --git a/src/landing/.web/utils/helpers/range.js b/src/landing/.web/utils/helpers/range.js
new file mode 100644
index 0000000..7d1aeda
--- /dev/null
+++ b/src/landing/.web/utils/helpers/range.js
@@ -0,0 +1,43 @@
+/**
+ * Simulate the python range() builtin function.
+ * inspired by https://dev.to/guyariely/using-python-range-in-javascript-337p
+ *
+ * If needed outside of an iterator context, use `Array.from(range(10))` or
+ * spread syntax `[...range(10)]` to get an array.
+ *
+ * @param {number} start: the start or end of the range.
+ * @param {number} stop: the end of the range.
+ * @param {number} step: the step of the range.
+ * @returns {object} an object with a Symbol.iterator method over the range
+ */
+export default function range(start, stop, step) {
+ return {
+ [Symbol.iterator]() {
+ if (stop === undefined) {
+ stop = start;
+ start = 0;
+ }
+ if (step === undefined) {
+ step = 1;
+ }
+
+ let i = start - step;
+
+ return {
+ next() {
+ i += step;
+ if ((step > 0 && i < stop) || (step < 0 && i > stop)) {
+ return {
+ value: i,
+ done: false,
+ };
+ }
+ return {
+ value: undefined,
+ done: true,
+ };
+ },
+ };
+ },
+ };
+ }
\ No newline at end of file
diff --git a/src/landing/.web/utils/state.js b/src/landing/.web/utils/state.js
new file mode 100644
index 0000000..088f256
--- /dev/null
+++ b/src/landing/.web/utils/state.js
@@ -0,0 +1,668 @@
+// State management for Reflex web apps.
+import axios from "axios";
+import io from "socket.io-client";
+import JSON5 from "json5";
+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"
+
+// Endpoint URLs.
+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"]
+
+// Global variable to hold the token.
+let token;
+
+// Key for the token in the session storage.
+const TOKEN_KEY = "token";
+
+// create cookie instance
+const cookies = new Cookies();
+
+// Dictionary holding component references.
+export const refs = {};
+
+// Flag ensures that only one event is processing on the backend concurrently.
+let event_processing = false
+// Array holding pending events to be processed.
+const event_queue = [];
+
+// Pending upload promises, by id
+const upload_controllers = {};
+
+/**
+ * Generate a UUID (Used for session tokens).
+ * Taken from: https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid
+ * @returns A UUID.
+ */
+export const generateUUID = () => {
+ let d = new Date().getTime(),
+ d2 = (performance && performance.now && performance.now() * 1000) || 0;
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
+ let r = Math.random() * 16;
+ if (d > 0) {
+ r = (d + r) % 16 | 0;
+ d = Math.floor(d / 16);
+ } else {
+ r = (d2 + r) % 16 | 0;
+ d2 = Math.floor(d2 / 16);
+ }
+ return (c == "x" ? r : (r & 0x7) | 0x8).toString(16);
+ });
+};
+
+/**
+ * Get the token for the current session.
+ * @returns The token.
+ */
+export const getToken = () => {
+ if (token) {
+ return token;
+ }
+ if (typeof window !== 'undefined') {
+ if (!window.sessionStorage.getItem(TOKEN_KEY)) {
+ window.sessionStorage.setItem(TOKEN_KEY, generateUUID());
+ }
+ token = window.sessionStorage.getItem(TOKEN_KEY);
+ }
+ return token;
+};
+
+/**
+ * Get the URL for the backend server
+ * @param url_str The URL string to parse.
+ * @returns The given URL modified to point to the actual backend server.
+ */
+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)) {
+ // Use the frontend domain to access the backend
+ const frontend_hostname = window.location.hostname;
+ endpoint.hostname = frontend_hostname;
+ if (window.location.protocol === "https:") {
+ if (endpoint.protocol === "ws:") {
+ endpoint.protocol = "wss:";
+ } else if (endpoint.protocol === "http:") {
+ endpoint.protocol = "https:";
+ }
+ endpoint.port = ""; // Assume websocket is on https port via load balancer.
+ }
+ }
+ return endpoint
+}
+
+/**
+ * Apply a delta to the state.
+ * @param state The state to apply the delta to.
+ * @param delta The delta to apply.
+ */
+export const applyDelta = (state, delta) => {
+ return { ...state, ...delta }
+};
+
+
+/**
+ * Handle frontend event or send the event to the backend via Websocket.
+ * @param event The event to send.
+ * @param socket The socket object to send the event on.
+ *
+ * @returns True if the event was sent, false if it was handled locally.
+ */
+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);
+ return false;
+ }
+
+ if (event.name == "_console") {
+ console.log(event.payload.message);
+ return false;
+ }
+
+ if (event.name == "_remove_cookie") {
+ cookies.remove(event.payload.key, { ...event.payload.options })
+ queueEvents(initialEvents(), socket)
+ return false;
+ }
+
+ if (event.name == "_clear_local_storage") {
+ localStorage.clear();
+ queueEvents(initialEvents(), socket)
+ return false;
+ }
+
+ if (event.name == "_remove_local_storage") {
+ localStorage.removeItem(event.payload.key);
+ queueEvents(initialEvents(), socket)
+ return false;
+ }
+
+ if (event.name == "_set_clipboard") {
+ const content = event.payload.content;
+ navigator.clipboard.writeText(content);
+ return false;
+ }
+
+ if (event.name == "_download") {
+ const a = document.createElement('a');
+ a.hidden = true;
+ a.href = event.payload.url
+ a.download = event.payload.filename;
+ a.click();
+ a.remove();
+ return false;
+ }
+
+ if (event.name == "_alert") {
+ alert(event.payload.message);
+ return false;
+ }
+
+ if (event.name == "_set_focus") {
+ const ref =
+ event.payload.ref in refs ? refs[event.payload.ref] : event.payload.ref;
+ ref.current.focus();
+ return false;
+ }
+
+ 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;
+ return false;
+ }
+
+ if (event.name == "_call_script") {
+ 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)
+ } else {
+ eval(event.payload.callback)(eval_result)
+ }
+ }
+ } catch (e) {
+ console.log("_call_script", e);
+ }
+ return false;
+ }
+
+ // 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)
+ }
+
+ // Send the event to the server.
+ if (socket) {
+ socket.emit("event", JSON.stringify(event, (k, v) => v === undefined ? null : v));
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Send an event to the server via REST.
+ * @param event The current event.
+ * @param socket The socket object to send the response event(s) on.
+ *
+ * @returns Whether the event was sent.
+ */
+export const applyRestEvent = async (event, socket) => {
+ let eventSent = false;
+ if (event.handler == "uploadFiles") {
+ // Start upload, but do not wait for it, which would block other events.
+ uploadFiles(
+ event.name,
+ event.payload.files,
+ event.payload.upload_id,
+ event.payload.on_upload_progress,
+ socket
+ );
+ return false;
+ }
+ return eventSent;
+};
+
+/**
+ * Queue events to be processed and trigger processing of queue.
+ * @param events Array of events to queue.
+ * @param socket The socket object to send the event on.
+ */
+export const queueEvents = async (events, socket) => {
+ 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
+) => {
+ // Only proceed if the socket is up, otherwise we throw the event into the void
+ if (!socket) {
+ return;
+ }
+
+ // Only proceed if we're not already processing an event.
+ if (event_queue.length === 0 || event_processing) {
+ return;
+ }
+
+ // Set processing to true to block other events from being processed.
+ event_processing = true
+
+ // Apply the next event in the queue.
+ const event = event_queue.shift();
+
+ let eventSent = false
+ // Process events with handlers via REST and all others via websockets.
+ if (event.handler) {
+ eventSent = await applyRestEvent(event, socket);
+ } else {
+ eventSent = await applyEvent(event, socket);
+ }
+ // If no event was sent, set processing to false.
+ if (!eventSent) {
+ 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)
+ }
+}
+
+/**
+ * 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 client_storage The client storage object from context.js
+ */
+export const connect = async (
+ socket,
+ dispatch,
+ transports,
+ setConnectError,
+ client_storage = {},
+) => {
+ // Get backend URL object from the endpoint.
+ const endpoint = getBackendURL(EVENTURL)
+
+ // Create the socket.
+ socket.current = io(endpoint.href, {
+ path: endpoint["pathname"],
+ transports: transports,
+ autoUnref: false,
+ });
+
+ // Once the socket is open, hydrate the page.
+ socket.current.on("connect", () => {
+ setConnectError(null)
+ });
+
+ socket.current.on('connect_error', (error) => {
+ setConnectError(error)
+ });
+
+ // On each received message, queue the updates and events.
+ socket.current.on("event", message => {
+ const update = JSON5.parse(message)
+ for (const substate in update.delta) {
+ dispatch[substate](update.delta[substate])
+ }
+ applyClientStorageDelta(client_storage, update.delta)
+ event_processing = !update.final
+ if (update.events) {
+ queueEvents(update.events, socket)
+ }
+ });
+};
+
+/**
+ * Upload files to the server.
+ *
+ * @param state The state to apply the delta to.
+ * @param handler The handler to use.
+ * @param upload_id The upload id to use.
+ * @param on_upload_progress The function to call on upload progress.
+ * @param socket the websocket connection
+ *
+ * @returns The response from posting to the UPLOADURL endpoint.
+ */
+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)
+ 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")
+ chunks.slice(resp_idx).map((chunk) => {
+ try {
+ socket._callbacks.$event.map((f) => {
+ 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)
+ }
+ return
+ }
+ })
+ }
+
+ const controller = new AbortController()
+ const config = {
+ headers: {
+ "Reflex-Client-Token": getToken(),
+ "Reflex-Event-Handler": handler,
+ },
+ signal: controller.signal,
+ onDownloadProgress: eventHandler,
+ }
+ if (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
+ );
+ })
+
+ // Send the file to the server.
+ upload_controllers[upload_id] = controller
+
+ try {
+ 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
+ // that falls out of the range of 2xx
+ console.log(error.response.data);
+ } else if (error.request) {
+ // The request was made but no response was received
+ // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
+ // http.ClientRequest in node.js
+ console.log(error.request);
+ } else {
+ // Something happened in setting up the request that triggered an Error
+ console.log(error.message);
+ }
+ return false;
+ } finally {
+ delete upload_controllers[upload_id]
+ }
+};
+
+/**
+ * Create an event object.
+ * @param name The name of the event.
+ * @param payload The payload of the event.
+ * @param handler The client handler to process event.
+ * @returns The event object.
+ */
+export const Event = (name, payload = {}, handler = null) => {
+ return { name, payload, handler };
+};
+
+/**
+ * Package client-side storage values as payload to send to the
+ * backend with the hydrate event
+ * @param client_storage The client storage object from context.js
+ * @returns payload dict of client storage values
+ */
+export const hydrateClientStorage = (client_storage) => {
+ 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)
+ if (cookie_value !== undefined) {
+ client_storage_values[state_key] = cookies.get(cookie_name)
+ }
+ }
+ }
+ 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)
+ if (local_storage_value !== null) {
+ client_storage_values[state_key] = local_storage_value
+ }
+ }
+ }
+ if (client_storage.cookies || client_storage.local_storage) {
+ return client_storage_values
+ }
+ return {}
+};
+
+/**
+ * Update client storage values based on backend state delta.
+ * @param client_storage The client storage object from context.js
+ * @param delta The state update from the backend
+ */
+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);
+ if (unqualified_states.length === 1) {
+ 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
+ return;
+ }
+ }
+ // 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}`
+ 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
+ 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]
+ localStorage.setItem(options.name || state_key, delta[substate][key]);
+ }
+ }
+ }
+}
+
+/**
+ * Establish websocket event loop for a NextJS page.
+ * @param dispatch The reducer dispatch function to update state.
+ * @param initial_events The initial app events.
+ * @param client_storage The client storage object from context.js
+ *
+ * @returns [addEvents, connectError] -
+ * addEvents is used to queue an event, and
+ * connectError is a reactive js error from the websocket connection (or null if connected).
+ */
+export const useEventLoop = (
+ dispatch,
+ initial_events = () => [],
+ client_storage = {},
+) => {
+ const socket = useRef(null)
+ const router = useRouter()
+ const [connectError, setConnectError] = useState(null)
+
+ // Function to add new events to the event queue.
+ const addEvents = (events, _e, event_actions) => {
+ if (event_actions?.preventDefault && _e?.preventDefault) {
+ _e.preventDefault();
+ }
+ if (event_actions?.stopPropagation && _e?.stopPropagation) {
+ _e.stopPropagation();
+ }
+ queueEvents(events, socket)
+ }
+
+ 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) => (
+ {
+ ...e,
+ router_data: (({ pathname, query, asPath }) => ({ pathname, query, asPath }))(router)
+ }
+ )))
+ sentHydrate.current = true
+ }
+ }, [router.isReady])
+
+ // Main event loop.
+ useEffect(() => {
+ // Skip if the router is not ready.
+ if (!router.isReady) {
+ return;
+ }
+ // only use websockets if state is present
+ if (Object.keys(initialState).length > 1) {
+ // Initialize the websocket connection.
+ if (!socket.current) {
+ connect(socket, dispatch, ['websocket', 'polling'], setConnectError, client_storage)
+ }
+ (async () => {
+ // Process all outstanding events.
+ while (event_queue.length > 0 && !event_processing) {
+ await processEvent(socket.current)
+ }
+ })()
+ }
+ })
+
+
+ // localStorage event handling
+ useEffect(() => {
+ const storage_to_state_map = {};
+
+ 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];
+ if (options.sync) {
+ const local_storage_value_key = options.name || state_key;
+ storage_to_state_map[local_storage_value_key] = state_key;
+ }
+ }
+ }
+
+ // 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})
+ addEvents([event], e);
+ }
+ };
+
+ window.addEventListener("storage", handleStorage);
+ return () => window.removeEventListener("storage", handleStorage);
+ });
+
+
+ // Route after the initial page hydration.
+ useEffect(() => {
+ const change_complete = () => addEvents(onLoadInternalEvent())
+ router.events.on('routeChangeComplete', change_complete)
+ return () => {
+ router.events.off('routeChangeComplete', change_complete)
+ }
+ }, [router])
+
+ return [addEvents, connectError]
+}
+
+/***
+ * Check if a value is truthy in python.
+ * @param val The value to check.
+ * @returns True if the value is truthy, false otherwise.
+ */
+export const isTrue = (val) => {
+ return Array.isArray(val) ? val.length > 0 : !!val;
+};
+
+/**
+ * Get the value from a ref.
+ * @param ref The ref to get the value from.
+ * @returns The value.
+ */
+export const getRefValue = (ref) => {
+ if (!ref || !ref.current) {
+ 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")) {
+ // find the actual slider
+ 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);
+ }
+}
+
+/**
+ * Get the values from a ref array.
+ * @param refs The refs to get the values from.
+ * @returns The values array.
+ */
+export const getRefValues = (refs) => {
+ if (!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);
+}
+
+/**
+* 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') {
+ return { ...first, ...second };
+ } else {
+ throw new Error('Both parameters must be either arrays or objects.');
+ }
+}
diff --git a/src/landing/landing/components/__init__.py b/src/landing/landing/components/__init__.py
new file mode 100644
index 0000000..a830f66
--- /dev/null
+++ b/src/landing/landing/components/__init__.py
@@ -0,0 +1,2 @@
+from .navbar import navbar
+from .footer import footer
diff --git a/src/landing/landing/components/footer.py b/src/landing/landing/components/footer.py
new file mode 100644
index 0000000..f67375b
--- /dev/null
+++ b/src/landing/landing/components/footer.py
@@ -0,0 +1,20 @@
+import reflex as rx
+
+def footer():
+ return rx.box(
+ footer_content(),
+ background="#FFF",
+ border_top=f"8px solid {rx.color('mauve', 4)};"
+ )
+
+
+
+def footer_content():
+ return rx.center(
+ rx.vstack(
+ rx.heading("Footer", size="9"),
+ align="center",
+ spacing="7"
+ ),
+ height="15vh"
+ )
diff --git a/src/landing/landing/components/navbar.py b/src/landing/landing/components/navbar.py
new file mode 100644
index 0000000..fbc5537
--- /dev/null
+++ b/src/landing/landing/components/navbar.py
@@ -0,0 +1,16 @@
+import reflex as rx
+
+
+def navbar():
+ return rx.flex(
+ background = "#FFF",
+ border_bottom=f"8px solid {rx.color('mauve', 4)};",
+ height="10vh",
+ position="fixed",
+ width="100%",
+ top="0px",
+ z_index="5",
+ align_items= "center",
+ spacing="6",
+ padding= "7px 20px 7px 20px;",
+ )
diff --git a/src/landing/landing/landing.py b/src/landing/landing/landing.py
index 4a17519..f35327d 100644
--- a/src/landing/landing/landing.py
+++ b/src/landing/landing/landing.py
@@ -1,35 +1,16 @@
-"""Welcome to Reflex! This file outlines the steps to create a basic app."""
-
-from rxconfig import config
-
import reflex as rx
+from rxconfig import config
+from landing.state import *
+from landing.pages import *
-docs_url = "https://reflex.dev/docs/getting-started/introduction"
-filename = f"{config.app_name}/{config.app_name}.py"
+# Create app instance and add index page.
+app = rx.App(
+)
-
-class State(rx.State):
- """The app state."""
-
-
-def index() -> rx.Component:
- return rx.center(
- rx.theme_panel(),
- rx.vstack(
- rx.heading("Welcome to Reflex!", size="9"),
- rx.text("Get started by editing ", rx.code(filename)),
- rx.button(
- "Check out our docs!",
- on_click=lambda: rx.redirect(docs_url),
- size="4",
- ),
- align="center",
- spacing="7",
- font_size="2em",
- ),
- height="100vh",
+for route in routes:
+ app.add_page(
+ route.component,
+ route.path,
+ route.title,
+ #image="/previews/index_preview.png",
)
-
-
-app = rx.App()
-app.add_page(index)
diff --git a/src/landing/landing/pages/__init__.py b/src/landing/landing/pages/__init__.py
new file mode 100644
index 0000000..d0e5966
--- /dev/null
+++ b/src/landing/landing/pages/__init__.py
@@ -0,0 +1,10 @@
+from landing.route import Route
+
+from .index import index
+from .page404 import page404
+
+routes = [
+ *[r for r in locals().values() if isinstance(r, Route)],
+ #*blog_routes,
+ #*doc_routes,
+]
diff --git a/src/landing/landing/pages/index.py b/src/landing/landing/pages/index.py
new file mode 100644
index 0000000..76fafdd
--- /dev/null
+++ b/src/landing/landing/pages/index.py
@@ -0,0 +1,19 @@
+import reflex as rx
+from landing.components import navbar
+from landing.templates import webpage
+
+@webpage(path="/", title="Timothy Pidashev")
+def index() -> rx.Component:
+ return rx.box(
+ index_content()
+ )
+
+def index_content():
+ return rx.center(
+ rx.vstack(
+ rx.heading("Index", size="9"),
+ align="center",
+ spacing="7",
+ ),
+ height="100vh"
+ )
diff --git a/src/landing/landing/pages/page404.py b/src/landing/landing/pages/page404.py
new file mode 100644
index 0000000..9e78b0c
--- /dev/null
+++ b/src/landing/landing/pages/page404.py
@@ -0,0 +1,15 @@
+import reflex as rx
+from landing.templates import webpage
+
+# TODO: Add a go back here link
+
+@webpage(path="/404", title="Page Not Found")
+def page404():
+ return rx.center(
+ rx.vstack(
+ rx.heading("Whoops, this page doesn't exist...", size="9"),
+ rx.spacer(),
+ ),
+ height="100vh",
+ width="100%",
+ )
diff --git a/src/landing/landing/route.py b/src/landing/landing/route.py
new file mode 100644
index 0000000..89c2c6f
--- /dev/null
+++ b/src/landing/landing/route.py
@@ -0,0 +1,29 @@
+"""Manage routing for the application."""
+
+import inspect
+import reflex as rx
+from reflex.base import Base
+from typing import Callable
+
+class Route(Base):
+ """A Page Route."""
+
+ # The path of the route.
+ path: str
+
+ # The page title.
+ title: str | None = None
+
+ # The component to render for the route.
+ component: Callable[[], rx.Component]
+
+def get_path(component_function: Callable):
+ """Get the path for a page based on the file location.
+
+ Args:
+ component_function: The component function for the page.
+ """
+ module = inspect.getmodule(component_function)
+
+ # Create a path based on the module name.
+ return module.__name__.replace(".", "/").replace("_", "-").split("landing/pages")[1]
diff --git a/src/landing/landing/state/__init__.py b/src/landing/landing/state/__init__.py
new file mode 100644
index 0000000..0a2dce5
--- /dev/null
+++ b/src/landing/landing/state/__init__.py
@@ -0,0 +1,2 @@
+from .state import State
+from .theme import ThemeState
diff --git a/src/landing/landing/state/state.py b/src/landing/landing/state/state.py
new file mode 100644
index 0000000..bb3a217
--- /dev/null
+++ b/src/landing/landing/state/state.py
@@ -0,0 +1,5 @@
+import reflex as rx
+
+class State(rx.State):
+ """The app state."""
+ pass
diff --git a/src/landing/landing/state/theme.py b/src/landing/landing/state/theme.py
new file mode 100644
index 0000000..1f3631f
--- /dev/null
+++ b/src/landing/landing/state/theme.py
@@ -0,0 +1,10 @@
+import reflex as rx
+
+from .state import State
+
+class ThemeState(State):
+ """State for the global theme"""
+ theme: str = "day"
+
+ def toggle_theme(self):
+ self.theme == "day" if self.theme != "day" else self.theme == "night"
diff --git a/src/landing/landing/templates/__init__.py b/src/landing/landing/templates/__init__.py
new file mode 100644
index 0000000..cac9af0
--- /dev/null
+++ b/src/landing/landing/templates/__init__.py
@@ -0,0 +1 @@
+from .webpage import webpage
diff --git a/src/landing/landing/templates/webpage.py b/src/landing/landing/templates/webpage.py
new file mode 100644
index 0000000..ce6fb1d
--- /dev/null
+++ b/src/landing/landing/templates/webpage.py
@@ -0,0 +1,56 @@
+from typing import Callable
+import reflex as rx
+from landing.route import Route
+
+def webpage(path: str, title: str = "Timothy Pidashev", props=None) -> Callable:
+ """This template wraps the webpage with the navbar and footer.
+
+ Args:
+ path: The path of the page.
+ title: The title of the page.
+ props: Props to apply to the template.
+
+ Returns:
+ A wrapper function that returns the full webpage.
+ """
+ props = props or {}
+
+ def webpage(contents: Callable[[], Route]) -> Route:
+ """Wrapper to create a templated route.
+
+ Args:
+ contents: The function to create the page route.
+
+ Returns:
+ The templated route.
+ """
+
+ def wrapper(*children, **props) -> rx.Component:
+ """The template component.
+
+ Args:
+ children: The children components.
+ props: The props to apply to the component.
+
+ Returns:
+ The component with the template applied.
+ """
+ # Import here to avoid circular imports.
+ from landing.components.navbar import navbar
+ from landing.components.footer import footer
+
+ # Wrap the component in the template.
+ return rx.box(
+ navbar(),
+ contents(*children, **props),
+ footer(),
+ **props,
+ )
+
+ return Route(
+ path=path,
+ title=title,
+ component=wrapper,
+ )
+
+ return webpage