diff --git a/Caddyfile.dev b/Caddyfile.dev
index 7f50e52..2cff439 100644
--- a/Caddyfile.dev
+++ b/Caddyfile.dev
@@ -13,3 +13,19 @@ timmypidashev.localhost {
reverse_proxy landing:8000
}
}
+
+about.timmypidashev.localhost {
+ encode gzip
+
+ reverse_proxy about:3000
+
+ @backend_routes {
+ path /_event/*
+ path /_upload
+ path /ping
+ }
+
+ handle @backend_routes {
+ reverse_proxy about:8000
+ }
+}
diff --git a/Makefile b/Makefile
index 2dcee74..0f7ba7d 100644
--- a/Makefile
+++ b/Makefile
@@ -11,6 +11,11 @@ CONTAINER_LANDING_VERSION := "v1.0.0"
CONTAINER_LANDING_LOCATION := "src/landing"
CONTAINER_LANDING_DESCRIPTION := "The landing page for my website."
+CONTAINER_ABOUT_NAME := "about"
+CONTAINER_ABOUT_VERSION := "v0.0.0"
+CONTAINER_ABOUT_LOCATION := "src/about"
+CONTAINER_ABOUT_DESCRIPTION := "The about page for my website."
+
.DEFAULT_GOAL := help
.PHONY: run build push prune bump
.SILENT: run build push prune bump
diff --git a/compose.dev.yml b/compose.dev.yml
index 37b438e..93289a5 100644
--- a/compose.dev.yml
+++ b/compose.dev.yml
@@ -12,6 +12,9 @@ services:
restart: always
networks:
- caddy
+ depends_on:
+ - landing
+ - about
landing:
container_name: landing
@@ -22,7 +25,15 @@ services:
- ./src/landing/rxconfig.py:/app/rxconfig.py
networks:
- caddy
- depends_on:
+
+ about:
+ container_name: about
+ image: about:dev
+ volumes:
+ - ./src/about/about:/app/about
+ - ./src/about/assets:/app/assets
+ - ./src/about/rxconfig.py:/app/rxconfig.py
+ networks:
- caddy
networks:
diff --git a/src/about/.dockerignore b/src/about/.dockerignore
new file mode 100644
index 0000000..b289110
--- /dev/null
+++ b/src/about/.dockerignore
@@ -0,0 +1,3 @@
+.web
+__pycache__/*
+Dockerfile
diff --git a/src/about/.web/.gitignore b/src/about/.web/.gitignore
new file mode 100644
index 0000000..534bc86
--- /dev/null
+++ b/src/about/.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/about/.web/components/reflex/chakra_color_mode_provider.js b/src/about/.web/components/reflex/chakra_color_mode_provider.js
new file mode 100644
index 0000000..f897522
--- /dev/null
+++ b/src/about/.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/about/.web/components/reflex/radix_themes_color_mode_provider.js b/src/about/.web/components/reflex/radix_themes_color_mode_provider.js
new file mode 100644
index 0000000..e2dd563
--- /dev/null
+++ b/src/about/.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/about/.web/jsconfig.json b/src/about/.web/jsconfig.json
new file mode 100644
index 0000000..3c8a325
--- /dev/null
+++ b/src/about/.web/jsconfig.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["public/*"]
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/about/.web/next.config.js b/src/about/.web/next.config.js
new file mode 100644
index 0000000..42dad1d
--- /dev/null
+++ b/src/about/.web/next.config.js
@@ -0,0 +1 @@
+module.exports = {basePath: "", compress: true, reactStrictMode: true, trailingSlash: true};
diff --git a/src/about/.web/package.json b/src/about/.web/package.json
new file mode 100644
index 0000000..aab86df
--- /dev/null
+++ b/src/about/.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/about/.web/postcss.config.js b/src/about/.web/postcss.config.js
new file mode 100644
index 0000000..33ad091
--- /dev/null
+++ b/src/about/.web/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/src/about/.web/reflex.json b/src/about/.web/reflex.json
new file mode 100644
index 0000000..7aecab7
--- /dev/null
+++ b/src/about/.web/reflex.json
@@ -0,0 +1 @@
+{"version": "0.4.3", "project_hash": 326586468133229768661018683044141625593}
\ No newline at end of file
diff --git a/src/about/.web/styles/tailwind.css b/src/about/.web/styles/tailwind.css
new file mode 100644
index 0000000..b5c61c9
--- /dev/null
+++ b/src/about/.web/styles/tailwind.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/src/about/.web/utils/client_side_routing.js b/src/about/.web/utils/client_side_routing.js
new file mode 100644
index 0000000..75fb581
--- /dev/null
+++ b/src/about/.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/about/.web/utils/helpers/dataeditor.js b/src/about/.web/utils/helpers/dataeditor.js
new file mode 100644
index 0000000..9ff3682
--- /dev/null
+++ b/src/about/.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/about/.web/utils/helpers/range.js b/src/about/.web/utils/helpers/range.js
new file mode 100644
index 0000000..7d1aeda
--- /dev/null
+++ b/src/about/.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/about/.web/utils/state.js b/src/about/.web/utils/state.js
new file mode 100644
index 0000000..51e8c6b
--- /dev/null
+++ b/src/about/.web/utils/state.js
@@ -0,0 +1,728 @@
+// 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;
+ if (ref.current) {
+ 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 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,
+ setConnectErrors,
+ 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,
+ });
+
+ 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", () => {
+ setConnectErrors([]);
+ });
+
+ 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);
+ 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);
+ }
+ });
+
+ document.addEventListener("visibilitychange", checkVisibility);
+};
+
+/**
+ * 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, connectErrors] -
+ * addEvents is used to queue an event, and
+ * connectErrors is an array of 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 [connectErrors, setConnectErrors] = useState([]);
+
+ // 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"],
+ setConnectErrors,
+ 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_state.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, connectErrors];
+};
+
+/***
+ * 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 &&
+ 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/about/Dockerfile.dev b/src/about/Dockerfile.dev
new file mode 100644
index 0000000..f5c6b24
--- /dev/null
+++ b/src/about/Dockerfile.dev
@@ -0,0 +1,54 @@
+# Stage 1: init
+FROM python:3.11 as init
+
+# Copy local context to `/app` inside container (see .dockerignore)
+WORKDIR /app
+COPY . .
+
+# Create virtualenv which will be copied into the final container
+ENV VIRTUAL_ENV=/app/.venv
+ENV PATH="$VIRTUAL_ENV/bin:$PATH"
+RUN python3.11 -m venv $VIRTUAL_ENV
+
+# Install app requirements and reflex inside virtualenv
+RUN pip install -r requirements.txt
+
+# Deploy templates and prepare app
+RUN reflex init
+
+# Export static copy of frontend to /app/.web/_static
+RUN echo "Exporting reflex app to shrink the docker image size, not actually a prod build."
+RUN reflex export --frontend-only --no-zip
+
+# Copy static files out of /app to save space in backend image
+RUN mv .web/_static /tmp/_static
+RUN rm -rf .web && mkdir .web
+RUN mv /tmp/_static .web/_static
+
+# Stage 2: copy artifacts into slim image
+FROM python:3.11-slim
+WORKDIR /app
+RUN adduser --disabled-password --home /app reflex
+
+# Install Node.js and unzip
+RUN apt-get update && apt-get install -y nodejs unzip curl && curl -fsSL https://bun.sh/install | bash
+
+# Copy only the necessary files from the "init" stage
+COPY --chown=reflex --from=init /app/.venv /app/.venv
+COPY --chown=reflex --from=init /app/requirements.txt /app/requirements.txt
+COPY --chown=reflex --from=init /app /app
+
+# Change the ownership and permissions of /app/.local
+USER root
+RUN mkdir -p /app
+RUN chown -R reflex /app
+USER reflex
+
+# Activate the virtual environment and install application requirements
+ENV PATH="/app/.venv/bin:$PATH"
+RUN python3.11 -m venv /app/.venv
+RUN /app/.venv/bin/pip install -r /app/requirements.txt
+
+# The following lines are for the specific command for the application.
+CMD reflex init && reflex run --env dev
+
diff --git a/src/about/about/__init__.py b/src/about/about/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/about/about/about.py b/src/about/about/about.py
new file mode 100644
index 0000000..8ff44ab
--- /dev/null
+++ b/src/about/about/about.py
@@ -0,0 +1,35 @@
+"""Welcome to Reflex! This file outlines the steps to create a basic app."""
+
+from rxconfig import config
+
+import reflex as rx
+
+docs_url = "https://reflex.dev/docs/getting-started/introduction/"
+filename = f"{config.app_name}/{config.app_name}.py"
+
+
+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",
+ )
+
+
+app = rx.App()
+app.add_page(index)
diff --git a/src/about/assets/favicon.ico b/src/about/assets/favicon.ico
new file mode 100644
index 0000000..166ae99
Binary files /dev/null and b/src/about/assets/favicon.ico differ
diff --git a/src/about/requirements.txt b/src/about/requirements.txt
new file mode 100644
index 0000000..65c472d
--- /dev/null
+++ b/src/about/requirements.txt
@@ -0,0 +1 @@
+reflex==0.4.3
diff --git a/src/about/rxconfig.py b/src/about/rxconfig.py
new file mode 100644
index 0000000..250c229
--- /dev/null
+++ b/src/about/rxconfig.py
@@ -0,0 +1,5 @@
+import reflex as rx
+
+config = rx.Config(
+ app_name="about",
+)
\ No newline at end of file
diff --git a/src/landing/rxconfig.py b/src/landing/rxconfig.py
index be33673..2a54299 100644
--- a/src/landing/rxconfig.py
+++ b/src/landing/rxconfig.py
@@ -2,5 +2,5 @@ import reflex as rx
config = rx.Config(
app_name="landing",
- api_url="http://localhost:8000",
+ api_url="http://localhost:8000",
)