sometimes simpler is better; back to spa design

This commit is contained in:
Timothy Pidashev
2024-03-11 22:19:02 -07:00
parent 8b6a760d91
commit e7f70b4c02
102 changed files with 28 additions and 1171 deletions
+3
View File
@@ -0,0 +1,3 @@
.web
__pycache__/*
Dockerfile
+39
View File
@@ -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
BIN
View File
Binary file not shown.
@@ -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 (
<ColorModeContext.Provider value={[ colorMode, toggleColorMode ]}>
{children}
</ColorModeContext.Provider>
)
}
@@ -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 (
<ColorModeContext.Provider value={[ colorMode, toggleColorMode ]}>
{children}
</ColorModeContext.Provider>
)
}
+1
View File
@@ -0,0 +1 @@
{"PING": "http://localhost:8000/ping", "EVENT": "ws://localhost:8000/_event", "UPLOAD": "http://localhost:8000/_upload"}
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["public/*"]
}
}
}
+1
View File
@@ -0,0 +1 @@
module.exports = {basePath: "", compress: true, reactStrictMode: true, trailingSlash: true};
+29
View File
@@ -0,0 +1,29 @@
{
"name": "reflex",
"scripts": {
"dev": "next dev",
"export": "next build",
"export-sitemap": "next build && next-sitemap",
"prod": "next start"
},
"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",
"tailwindcss": "3.3.2"
}
}
+162
View File
@@ -0,0 +1,162 @@
/** @jsxImportSource @emotion/react */
import { Fragment, useContext } from "react"
import { EventLoopContext, StateContexts } from "/utils/context"
import { Event, getBackendURL, isTrue } from "/utils/state"
import { WifiOffIcon as LucideWifiOffIcon } from "lucide-react"
import { keyframes } from "@emotion/react"
import { Box as RadixThemesBox, Dialog as RadixThemesDialog, Flex as RadixThemesFlex, Heading as RadixThemesHeading, Link as RadixThemesLink, Text as RadixThemesText } from "@radix-ui/themes"
import env from "/env.json"
import NextLink from "next/link"
import NextHead from "next/head"
export function Fragment_966c0378eb9d65bdfb5286644be9b831 () {
const [addEvents, connectErrors] = useContext(EventLoopContext);
const state = useContext(StateContexts.state)
return (
<Fragment>
{isTrue(((!state.is_hydrated) || (connectErrors.length > 0))) ? (
<Fragment>
<LucideWifiOffIcon css={{"color": "crimson", "zIndex": 9999, "position": "fixed", "bottom": "30px", "right": "30px", "animation": `${pulse} 1s infinite`}} size={32}>
{`wifi_off`}
</LucideWifiOffIcon>
</Fragment>
) : (
<Fragment/>
)}
</Fragment>
)
}
export function Fragment_14636cc997c0546c0967a25d8e600f96 () {
const [addEvents, connectErrors] = useContext(EventLoopContext);
return (
<Fragment>
{isTrue(connectErrors.length >= 2) ? (
<Fragment>
<RadixThemesDialog.Root css={{"zIndex": 9999}} open={connectErrors.length >= 2}>
<RadixThemesDialog.Content>
<RadixThemesDialog.Title>
{`Connection Error`}
</RadixThemesDialog.Title>
<RadixThemesText as={`p`} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#ebdbb2"}}>
{`Cannot connect to server: `}
{(connectErrors.length > 0) ? connectErrors[connectErrors.length - 1].message : ''}
{`. Check if server is reachable at `}
{getBackendURL(env.EVENT).href}
</RadixThemesText>
</RadixThemesDialog.Content>
</RadixThemesDialog.Root>
</Fragment>
) : (
<Fragment/>
)}
</Fragment>
)
}
const pulse = keyframes`
0% {
opacity: 0;
}
100% {
opacity: 1;
}
`
export default function Component() {
return (
<Fragment>
<Fragment>
<div css={{"position": "fixed", "width": "100vw", "height": "0"}}>
<Fragment_966c0378eb9d65bdfb5286644be9b831/>
</div>
<Fragment_14636cc997c0546c0967a25d8e600f96/>
</Fragment>
<RadixThemesBox>
<RadixThemesBox>
<RadixThemesFlex css={{"display": "flex", "alignItems": "center", "justifyContent": "center"}} gap={`7`}>
<RadixThemesFlex>
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
<NextLink href={`http://about.timmypidashev.localhost`} passHref={true}>
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
{`About`}
</RadixThemesText>
</NextLink>
</RadixThemesLink>
</RadixThemesFlex>
<RadixThemesFlex>
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
<NextLink href={`http://projects.timmypidashev.localhost`} passHref={true}>
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
{`Projects`}
</RadixThemesText>
</NextLink>
</RadixThemesLink>
</RadixThemesFlex>
<RadixThemesFlex>
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
<NextLink href={`http://resume.timmypidashev.localhost`} passHref={true}>
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
{`Resume`}
</RadixThemesText>
</NextLink>
</RadixThemesLink>
</RadixThemesFlex>
<RadixThemesFlex>
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
<NextLink href={`http://blog.timmypidashev.localhost`} passHref={true}>
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
{`Blog`}
</RadixThemesText>
</NextLink>
</RadixThemesLink>
</RadixThemesFlex>
<RadixThemesFlex>
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
<NextLink href={`http://shop.timmypidashev.localhost`} passHref={true}>
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
{`Shop`}
</RadixThemesText>
</NextLink>
</RadixThemesLink>
</RadixThemesFlex>
</RadixThemesFlex>
</RadixThemesBox>
<RadixThemesFlex css={{"height": "100vh", "width": "100%", "display": "flex", "alignItems": "center", "justifyContent": "center"}}>
<RadixThemesFlex align={`start`} direction={`column`} gap={`2`}>
<RadixThemesHeading css={{"fontFamily": "ComicCode", "fontSize": 32, "color": "#ebdbb2"}} size={`9`}>
{`Whoops, this page doesn't exist...`}
</RadixThemesHeading>
<RadixThemesFlex css={{"flex": 1, "justifySelf": "stretch", "alignSelf": "stretch"}}/>
</RadixThemesFlex>
</RadixThemesFlex>
<RadixThemesBox css={{"borderTop": "2px solid #ebdbb2;"}}>
<RadixThemesFlex css={{"height": "15vh", "display": "flex", "alignItems": "center", "justifyContent": "center"}}>
<RadixThemesFlex align={`center`} direction={`column`} gap={`7`}>
<RadixThemesHeading css={{"fontFamily": "ComicCode", "fontSize": 32, "color": "#ebdbb2"}} size={`9`}>
{`Footer`}
</RadixThemesHeading>
</RadixThemesFlex>
</RadixThemesFlex>
</RadixThemesBox>
</RadixThemesBox>
<NextHead>
<title>
{`Page Not Found`}
</title>
<meta content={`A Reflex app.`} name={`description`}/>
<meta content={`favicon.ico`} property={`og:image`}/>
</NextHead>
</Fragment>
)
}
+44
View File
@@ -0,0 +1,44 @@
/** @jsxImportSource @emotion/react */
import '/styles/styles.css'
import RadixThemesColorModeProvider from "/components/reflex/radix_themes_color_mode_provider.js"
import { Theme as RadixThemesTheme } from "@radix-ui/themes"
import "@radix-ui/themes/styles.css"
import theme from "/utils/theme.js"
import { Fragment } from "react"
import { EventLoopProvider, StateProvider, defaultColorMode } from "/utils/context.js";
import { ThemeProvider } from 'next-themes'
function AppWrap({children}) {
return (
<RadixThemesColorModeProvider>
<RadixThemesTheme accentColor={`blue`} css={{...theme.styles.global[':root'], ...theme.styles.global.body}}>
<Fragment>
{children}
</Fragment>
</RadixThemesTheme>
</RadixThemesColorModeProvider>
)
}
export default function MyApp({ Component, pageProps }) {
return (
<ThemeProvider defaultTheme={ defaultColorMode } storageKey="chakra-ui-color-mode" attribute="class">
<AppWrap>
<StateProvider>
<EventLoopProvider>
<Component {...pageProps} />
</EventLoopProvider>
</StateProvider>
</AppWrap>
</ThemeProvider>
);
}
+18
View File
@@ -0,0 +1,18 @@
/** @jsxImportSource @emotion/react */
import { Head, Html, Main, NextScript } from "next/document"
export default function Document() {
return (
<Html>
<Head/>
<body>
<Main/>
<NextScript/>
</body>
</Html>
)
}
+163
View File
@@ -0,0 +1,163 @@
/** @jsxImportSource @emotion/react */
import { Fragment, useContext } from "react"
import { EventLoopContext, StateContexts } from "/utils/context"
import { Event, getBackendURL, isTrue } from "/utils/state"
import { WifiOffIcon as LucideWifiOffIcon } from "lucide-react"
import { keyframes } from "@emotion/react"
import { Box as RadixThemesBox, Dialog as RadixThemesDialog, Flex as RadixThemesFlex, Heading as RadixThemesHeading, Link as RadixThemesLink, Text as RadixThemesText } from "@radix-ui/themes"
import env from "/env.json"
import NextLink from "next/link"
import NextHead from "next/head"
export function Fragment_966c0378eb9d65bdfb5286644be9b831 () {
const [addEvents, connectErrors] = useContext(EventLoopContext);
const state = useContext(StateContexts.state)
return (
<Fragment>
{isTrue(((!state.is_hydrated) || (connectErrors.length > 0))) ? (
<Fragment>
<LucideWifiOffIcon css={{"color": "crimson", "zIndex": 9999, "position": "fixed", "bottom": "30px", "right": "30px", "animation": `${pulse} 1s infinite`}} size={32}>
{`wifi_off`}
</LucideWifiOffIcon>
</Fragment>
) : (
<Fragment/>
)}
</Fragment>
)
}
export function Fragment_14636cc997c0546c0967a25d8e600f96 () {
const [addEvents, connectErrors] = useContext(EventLoopContext);
return (
<Fragment>
{isTrue(connectErrors.length >= 2) ? (
<Fragment>
<RadixThemesDialog.Root css={{"zIndex": 9999}} open={connectErrors.length >= 2}>
<RadixThemesDialog.Content>
<RadixThemesDialog.Title>
{`Connection Error`}
</RadixThemesDialog.Title>
<RadixThemesText as={`p`} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#ebdbb2"}}>
{`Cannot connect to server: `}
{(connectErrors.length > 0) ? connectErrors[connectErrors.length - 1].message : ''}
{`. Check if server is reachable at `}
{getBackendURL(env.EVENT).href}
</RadixThemesText>
</RadixThemesDialog.Content>
</RadixThemesDialog.Root>
</Fragment>
) : (
<Fragment/>
)}
</Fragment>
)
}
const pulse = keyframes`
0% {
opacity: 0;
}
100% {
opacity: 1;
}
`
export default function Component() {
return (
<Fragment>
<Fragment>
<div css={{"position": "fixed", "width": "100vw", "height": "0"}}>
<Fragment_966c0378eb9d65bdfb5286644be9b831/>
</div>
<Fragment_14636cc997c0546c0967a25d8e600f96/>
</Fragment>
<RadixThemesBox>
<RadixThemesBox>
<RadixThemesFlex css={{"display": "flex", "alignItems": "center", "justifyContent": "center"}} gap={`7`}>
<RadixThemesFlex>
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
<NextLink href={`http://about.timmypidashev.localhost`} passHref={true}>
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
{`About`}
</RadixThemesText>
</NextLink>
</RadixThemesLink>
</RadixThemesFlex>
<RadixThemesFlex>
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
<NextLink href={`http://projects.timmypidashev.localhost`} passHref={true}>
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
{`Projects`}
</RadixThemesText>
</NextLink>
</RadixThemesLink>
</RadixThemesFlex>
<RadixThemesFlex>
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
<NextLink href={`http://resume.timmypidashev.localhost`} passHref={true}>
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
{`Resume`}
</RadixThemesText>
</NextLink>
</RadixThemesLink>
</RadixThemesFlex>
<RadixThemesFlex>
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
<NextLink href={`http://blog.timmypidashev.localhost`} passHref={true}>
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
{`Blog`}
</RadixThemesText>
</NextLink>
</RadixThemesLink>
</RadixThemesFlex>
<RadixThemesFlex>
<RadixThemesLink asChild={true} css={{"fontFamily": "ComicCode", "fontSize": 24, "color": "#000000", "textDecoration": "none", "&:hover": {"color": "#b8bb26"}}}>
<NextLink href={`http://shop.timmypidashev.localhost`} passHref={true}>
<RadixThemesText as={`p`} css={{"color": "#ebdbb2", "fontFamily": "ComicCode", "fontSize": 24}}>
{`Shop`}
</RadixThemesText>
</NextLink>
</RadixThemesLink>
</RadixThemesFlex>
</RadixThemesFlex>
</RadixThemesBox>
<RadixThemesBox>
<RadixThemesFlex css={{"height": "100vh", "display": "flex", "alignItems": "center", "justifyContent": "center"}}>
<RadixThemesFlex align={`center`} direction={`column`} gap={`7`}>
<RadixThemesHeading css={{"fontFamily": "ComicCode", "fontSize": 32, "color": "#ebdbb2"}} size={`9`}>
{`Index`}
</RadixThemesHeading>
</RadixThemesFlex>
</RadixThemesFlex>
</RadixThemesBox>
<RadixThemesBox css={{"borderTop": "2px solid #ebdbb2;"}}>
<RadixThemesFlex css={{"height": "15vh", "display": "flex", "alignItems": "center", "justifyContent": "center"}}>
<RadixThemesFlex align={`center`} direction={`column`} gap={`7`}>
<RadixThemesHeading css={{"fontFamily": "ComicCode", "fontSize": 32, "color": "#ebdbb2"}} size={`9`}>
{`Footer`}
</RadixThemesHeading>
</RadixThemesFlex>
</RadixThemesFlex>
</RadixThemesBox>
</RadixThemesBox>
<NextHead>
<title>
{`Timothy Pidashev`}
</title>
<meta content={`A Reflex app.`} name={`description`}/>
<meta content={`favicon.ico`} property={`og:image`}/>
</NextHead>
</Fragment>
)
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+9
View File
@@ -0,0 +1,9 @@
/* Hide scrollbar for all elements */
::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
/* Hide scrollbar for Firefox */
html {
scrollbar-width: none;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 407 KiB

+11
View File
@@ -0,0 +1,11 @@
@font-face {
font-family: ComicCode;
src: url("ComicCode-Regular.otf") format("opentype");
}
@font-face {
font-family: ComicCodeBold;
font-weight: bold;
src: url("ComicCode-Bold.otf") format("opentype");
}
@@ -0,0 +1 @@
['@radix-ui/themes@^2.0.0', 'lucide-react@0.314.0'],{"app_name": "landing", "loglevel": "info", "frontend_port": 3000, "frontend_path": "", "backend_port": 8000, "api_url": "http://localhost:8000", "deploy_url": "http://localhost:3000", "backend_host": "0.0.0.0", "db_url": "sqlite:///reflex.db", "redis_url": null, "telemetry_enabled": true, "bun_path": "/home/timmy/.local/share/reflex/bun/bin/bun", "cors_allowed_origins": ["*"], "tailwind": {"content": ["./pages/**/*.{js,ts,jsx,tsx}", "./utils/**/*.{js,ts,jsx,tsx}"]}, "timeout": 120, "next_compression": true, "event_namespace": null, "frontend_packages": [], "cp_backend_url": "https://rxcp-prod-control-plane.fly.dev", "cp_web_url": "https://control-plane.reflex.run", "gunicorn_worker_class": "uvicorn.workers.UvicornH11Worker"}
+1
View File
@@ -0,0 +1 @@
{"version": "0.4.3", "project_hash": 133404493286750418693195899744043836434}
+3
View File
@@ -0,0 +1,3 @@
@import url('./tailwind.css');
@import url('@/fonts/fonts.css');
@import url('@/css/scrollbar.css');
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+7
View File
@@ -0,0 +1,7 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./pages/**/*.{js,ts,jsx,tsx}", "./utils/**/*.{js,ts,jsx,tsx}"],
theme: null,
plugins: [
],
};
+36
View File
@@ -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
}
+8
View File
@@ -0,0 +1,8 @@
/** @jsxImportSource @emotion/react */
import { memo } from "react"
import { E, isTrue } from "/utils/state"
+112
View File
@@ -0,0 +1,112 @@
import { createContext, useContext, useMemo, useReducer, useState } from "react"
import { applyDelta, Event, hydrateClientStorage, useEventLoop, refs } from "/utils/state.js"
export const initialState = {"state": {"is_hydrated": false, "router": {"session": {"client_token": "", "client_ip": "", "session_id": ""}, "headers": {"host": "", "origin": "", "upgrade": "", "connection": "", "pragma": "", "cache_control": "", "user_agent": "", "sec_websocket_version": "", "sec_websocket_key": "", "sec_websocket_extensions": "", "accept_encoding": "", "accept_language": ""}, "page": {"host": "", "path": "", "raw_path": "", "full_path": "", "full_raw_path": "", "params": {}}}}, "state.on_load_internal_state": {}, "state.update_vars_internal_state": {}, "state.state": {}, "state.state.theme_state": {"current_theme": 0, "theme": {"background_color": "#282828"}, "themes": {"0": {"background_color": "#282828"}, "1": {"background_color": "#000000"}}}}
export const defaultColorMode = "light"
export const ColorModeContext = createContext(null);
export const UploadFilesContext = createContext(null);
export const DispatchContext = createContext(null);
export const StateContexts = {
state: createContext(null),
state__on_load_internal_state: createContext(null),
state__update_vars_internal_state: createContext(null),
state__state: createContext(null),
state__state__theme_state: createContext(null),
}
export const EventLoopContext = createContext(null);
export const clientStorage = {"cookies": {}, "local_storage": {}}
export const state_name = "state"
// Theses events are triggered on initial load and each page navigation.
export const onLoadInternalEvent = () => {
const internal_events = [];
// Get tracked cookie and local storage vars to send to the backend.
const client_storage_vars = hydrateClientStorage(clientStorage);
// But only send the vars if any are actually set in the browser.
if (client_storage_vars && Object.keys(client_storage_vars).length !== 0) {
internal_events.push(
Event(
'state.update_vars_internal_state.update_vars_internal',
{vars: client_storage_vars},
),
);
}
// `on_load_internal` triggers the correct on_load event(s) for the current page.
// If the page does not define any on_load event, this will just set `is_hydrated = true`.
internal_events.push(Event('state.on_load_internal_state.on_load_internal'));
return internal_events;
}
// The following events are sent when the websocket connects or reconnects.
export const initialEvents = () => [
Event('state.hydrate'),
...onLoadInternalEvent()
]
export const isDevMode = true
export function UploadFilesProvider({ children }) {
const [filesById, setFilesById] = useState({})
refs["__clear_selected_files"] = (id) => setFilesById(filesById => {
const newFilesById = {...filesById}
delete newFilesById[id]
return newFilesById
})
return (
<UploadFilesContext.Provider value={[filesById, setFilesById]}>
{children}
</UploadFilesContext.Provider>
)
}
export function EventLoopProvider({ children }) {
const dispatch = useContext(DispatchContext)
const [addEvents, connectErrors] = useEventLoop(
dispatch,
initialEvents,
clientStorage,
)
return (
<EventLoopContext.Provider value={[addEvents, connectErrors]}>
{children}
</EventLoopContext.Provider>
)
}
export function StateProvider({ children }) {
const [state, dispatch_state] = useReducer(applyDelta, initialState["state"])
const [state__on_load_internal_state, dispatch_state__on_load_internal_state] = useReducer(applyDelta, initialState["state.on_load_internal_state"])
const [state__update_vars_internal_state, dispatch_state__update_vars_internal_state] = useReducer(applyDelta, initialState["state.update_vars_internal_state"])
const [state__state, dispatch_state__state] = useReducer(applyDelta, initialState["state.state"])
const [state__state__theme_state, dispatch_state__state__theme_state] = useReducer(applyDelta, initialState["state.state.theme_state"])
const dispatchers = useMemo(() => {
return {
"state": dispatch_state,
"state.on_load_internal_state": dispatch_state__on_load_internal_state,
"state.update_vars_internal_state": dispatch_state__update_vars_internal_state,
"state.state": dispatch_state__state,
"state.state.theme_state": dispatch_state__state__theme_state,
}
}, [])
return (
<StateContexts.state.Provider value={ state }>
<StateContexts.state__on_load_internal_state.Provider value={ state__on_load_internal_state }>
<StateContexts.state__update_vars_internal_state.Provider value={ state__update_vars_internal_state }>
<StateContexts.state__state.Provider value={ state__state }>
<StateContexts.state__state__theme_state.Provider value={ state__state__theme_state }>
<DispatchContext.Provider value={dispatchers}>
{children}
</DispatchContext.Provider>
</StateContexts.state__state__theme_state.Provider>
</StateContexts.state__state.Provider>
</StateContexts.state__update_vars_internal_state.Provider>
</StateContexts.state__on_load_internal_state.Provider>
</StateContexts.state.Provider>
)
}
+69
View File
@@ -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 };
}
+43
View File
@@ -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,
};
},
};
},
};
}
+728
View File
@@ -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.");
}
};
@@ -0,0 +1,7 @@
/** @jsxImportSource @emotion/react */
+1
View File
@@ -0,0 +1 @@
export default {"styles": {"global": {":root": {}, "body": {"backgroundColor": "#282828"}}}}
+53
View File
@@ -0,0 +1,53 @@
# 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
+9
View File
@@ -0,0 +1,9 @@
/* Hide scrollbar for all elements */
::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
/* Hide scrollbar for Firefox */
html {
scrollbar-width: none;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
@font-face {
font-family: ComicCode;
src: url("ComicCode-Regular.otf") format("opentype");
}
@font-face {
font-family: ComicCodeBold;
font-weight: bold;
src: url("ComicCode-Bold.otf") format("opentype");
}
+1
View File
@@ -0,0 +1 @@
reflex==0.4.4
+6
View File
@@ -0,0 +1,6 @@
import reflex as rx
config = rx.Config(
app_name="web",
api_url="http://localhost:8000",
)
View File
+2
View File
@@ -0,0 +1,2 @@
from .navbar import navbar
from .footer import footer
+20
View File
@@ -0,0 +1,20 @@
import reflex as rx
from web.style import *
def footer():
return rx.box(
footer_content(),
border_top=f"2px solid {color['white']};"
)
def footer_content():
return rx.center(
rx.vstack(
rx.heading("Footer", size="9"),
align="center",
spacing="7"
),
height="15vh"
)
+39
View File
@@ -0,0 +1,39 @@
import reflex as rx
from web.style import *
def navbar():
return rx.box(
rx.center(
rx.flex(
rx.link(
rx.text("About", color=color["white"]),
href="http://about.timmypidashev.localhost"
)
),
rx.flex(
rx.link(
rx.text("Projects", color=color["white"]),
href="http://projects.timmypidashev.localhost"
)
),
rx.flex(
rx.link(
rx.text("Resume", color=color["white"]),
href="http://resume.timmypidashev.localhost"
)
),
rx.flex(
rx.link(
rx.text("Blog", color=color["white"]),
href="http://blog.timmypidashev.localhost"
)
),
rx.flex(
rx.link(
rx.text("Shop", color=color["white"]),
href="http://shop.timmypidashev.localhost"
)
),
spacing="7",
)
)
+10
View File
@@ -0,0 +1,10 @@
from web.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,
]
+19
View File
@@ -0,0 +1,19 @@
import reflex as rx
from web.components import navbar
from web.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"
)
+15
View File
@@ -0,0 +1,15 @@
import reflex as rx
from web.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%",
)
+29
View File
@@ -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("web/pages")[1]
+2
View File
@@ -0,0 +1,2 @@
from .state import State
from .theme import ThemeState
+5
View File
@@ -0,0 +1,5 @@
import reflex as rx
class State(rx.State):
"""The app state."""
pass
+19
View File
@@ -0,0 +1,19 @@
import reflex as rx
from .state import State
from web.style import *
from typing import Dict, Any, List
class ThemeState(State):
"""App Theme State"""
current_theme: int = 0
themes = {
0: {"background_color": "#282828"},
1: {"background_color": "#000000"},
}
@rx.var
def theme(self) -> dict:
return self.themes[self.current_theme]
+91
View File
@@ -0,0 +1,91 @@
import reflex as rx
color = {
"white": "#ebdbb2",
"black": "#000000",
"red": {
100: "#fb4934",
200: "#cc241d",
},
"green": {
100: "#b8bb26",
200: "#98971a",
},
"yellow": {
100: "#fabd2f",
200: "#d79921",
},
"blue": {
100: "#83a598",
200: "#458588",
},
"purple": {
100: "#d3869b",
200: "#b16286",
},
"aqua": {
100: "#8ec07c",
200: "#689d6a",
}
}
base_style = {
# Background
# TODO: Implement dynamic background switching once reflex allows for Dict state management
"background_color": "#282828",
# Text
rx.text: {
"font_family": "ComicCode",
"font_size": 24,
"color": color["white"]
},
# Heading
rx.heading: {
"font_family": "ComicCode",
"font_size": 32,
"color": color["white"]
},
# Link
rx.link: {
"font_family": "ComicCode",
"font_size": 24,
"color": color["black"],
"text_decoration": "none",
"_hover": {
"color": color["green"][100]
}
},
}
# Dark Theme
#dark_theme = dict()
#dark_theme["background_color"] = "#282828"
# Soft Contrast Dark Theme
# TODO
# Medium Contrast Dark Theme
# TODO
# Hard Contrast Dark Theme
# TODO
# Amoled Contrast Dark Theme
#amoled_dark_theme = dict()
#amoled_dark_theme = "#000000"
# Light Theme
# TODO
# Soft Contrast Light Theme
# TODO
# Medium Contrast Light Theme
# TODO
# Hard Contrast Light Theme
# TODO
+1
View File
@@ -0,0 +1 @@
from .webpage import webpage
+56
View File
@@ -0,0 +1,56 @@
from typing import Callable
import reflex as rx
from web.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 web.components.navbar import navbar
from web.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
+22
View File
@@ -0,0 +1,22 @@
import reflex as rx
from rxconfig import config
from web.state import *
from web.pages import *
from web.style import *
# Create app instance and add index page.
app = rx.App(
style=base_style,
stylesheets=[
"fonts/fonts.css",
"css/scrollbar.css"
]
)
for route in routes:
app.add_page(
route.component,
route.path,
route.title,
#image="/previews/index_preview.png",
)