From 6df9e9b3ee97069ea9bc19080a4a87b92efa4337 Mon Sep 17 00:00:00 2001 From: Antonette Caldwell <18711313+acald-creator@users.noreply.github.com> Date: Mon, 13 Apr 2026 02:07:15 -0500 Subject: [PATCH] feat: add client-side routing and page structure Add RescriptReactRouter with pattern-matched URL routing. Split monolithic App.res into separate pages (Home, Components, Posts, Login) with a sticky NavBar and 404 fallback. Add historyApiFallback to Rspack dev server for client-side routing support. Closes #36 --- rspack.config.js | 3 + src/App.res | 266 ++----------------------------- src/Router.res | 43 +++++ src/components/NavBar/NavBar.res | 88 ++++++++++ src/pages/ComponentsPage.res | 120 ++++++++++++++ src/pages/HomePage.res | 127 +++++++++++++++ src/pages/LoginPage.res | 21 +++ src/pages/PostsPage.res | 21 +++ 8 files changed, 438 insertions(+), 251 deletions(-) create mode 100644 src/Router.res create mode 100644 src/components/NavBar/NavBar.res create mode 100644 src/pages/ComponentsPage.res create mode 100644 src/pages/HomePage.res create mode 100644 src/pages/LoginPage.res create mode 100644 src/pages/PostsPage.res diff --git a/rspack.config.js b/rspack.config.js index e4fdee3..f39caee 100644 --- a/rspack.config.js +++ b/rspack.config.js @@ -20,6 +20,9 @@ module.exports = { entry: { main: "./src/main.bs.js", }, + devServer: { + historyApiFallback: true, + }, devtool: "source-map", module: { rules: [ diff --git a/src/App.res b/src/App.res index aecc48a..27f4431 100644 --- a/src/App.res +++ b/src/App.res @@ -1,14 +1,5 @@ -// Import design system and components open Emotion.Css -// Import component library -module Components = { - module Button = Button.Button - module Input = Input.Input - module Card = Card.Card - module Typography = Typography.Typography -} - // Initialize global styles let _ = GlobalStyles.initialize() @@ -24,7 +15,7 @@ Emotion.Css.injectGlobal(` will-change: filter; } .logo:hover { - filter: drop-shadow(0 0 2em #c8a45caa); + filter: drop-shadow(0 0 2em ${Theme.Theme.Colors.brand["primary"]}aa); } .logo.react:hover { filter: drop-shadow(0 0 2em #61dafbaa); @@ -47,7 +38,7 @@ Emotion.Css.injectGlobal(` /* Hero section gradients */ .hero-gradient { - background: linear-gradient(135deg, #c8a45c 0%, #c47d4a 100%); + background: linear-gradient(135deg, ${Theme.Theme.Colors.brand["primary"]} 0%, ${Theme.Theme.Colors.brand["secondary"]} 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -66,251 +57,24 @@ Emotion.Css.injectGlobal(` } `) -module Logo = { - @module("./assets/react.svg") @react.component - external make: (~role: string=?, ~className: string=?, ~alt: string=?, unit) => React.element = - "default" -} - -// Homepage sections -module HeroSection = { - @react.component - let make = () => { - let (count, setCount) = React.useState(() => 0) - - let heroStyles = css({ - "minHeight": "80vh", - "display": "flex", - "flexDirection": "column", - "alignItems": "center", - "justifyContent": "center", - "textAlign": "center", - "padding": "2rem 1rem", - "background": "linear-gradient(180deg, rgba(200, 164, 92, 0.04) 0%, transparent 100%)" - }) - - let logoContainerStyles = css({ - "display": "flex", - "gap": "2rem", - "marginBottom": "2rem", - "alignItems": "center" - }) - -
-
- - - -
- - - {React.string("Modern React Development")} - - - - {React.string("Built with Rspack, ReScript, React, and Bun for lightning-fast development")} - - -
- setCount(prev => prev + 1)}> - {React.string("Interactive Demo (" ++ Int.toString(count) ++ ")")} - - - {React.string("View Components")} - -
-
- } -} - -module FeaturesSection = { - @react.component - let make = () => { - let sectionStyles = css({ - "padding": "4rem 1rem", - "maxWidth": "1200px", - "margin": "0 auto" - }) - - let gridStyles = css({ - "display": "grid", - "gridTemplateColumns": "repeat(auto-fit, minmax(300px, 1fr))", - "gap": "2rem", - "marginTop": "3rem" - }) - - let features = [ - ("⚡ Lightning Fast", "Built with Rspack for ultra-fast bundling and hot reload"), - ("🔧 Type Safety", "ReScript provides bulletproof type safety and excellent ergonomics"), - ("🎨 Design System", "Comprehensive component library with consistent styling"), - ("🚀 Modern Stack", "Latest React, Emotion CSS-in-JS, and Bun package manager") - ] - -
- - {React.string("Why This Template?")} - - -
- {features - ->Belt.Array.map(((title, description)) => { - - - {React.string(title)} - - - {React.string(description)} - - - }) - ->React.array} -
-
- } -} - -module ComponentsSection = { - @react.component - let make = () => { - let (inputValue, setInputValue) = React.useState(() => "") - let (_selectedCard, _setSelectedCard) = React.useState(() => None) - - let sectionStyles = css({ - "padding": "4rem 1rem", - "backgroundColor": `${Theme.Theme.Colors.background["elevated"]}80`, - "borderTop": `1px solid ${Theme.Theme.Colors.border["default"]}` - }) - - let containerStyles = css({ - "maxWidth": "1200px", - "margin": "0 auto" - }) - - let demoGridStyles = css({ - "display": "grid", - "gridTemplateColumns": "repeat(auto-fit, minmax(350px, 1fr))", - "gap": "2rem", - "marginTop": "3rem" - }) - -
-
- - {React.string("Component Library")} - - - - {React.string("Explore our comprehensive component library with variants, sizes, and states")} - - -
- // Button Variants Demo - - - - {React.string("Button Variants")} - - - -
- - {React.string("Primary")} - - - {React.string("Secondary")} - - - {React.string("Outline")} - - - {React.string("Ghost")} - - - {React.string("Danger")} - -
-
-
- - // Input Variants Demo - - - - {React.string("Input Variants")} - - - -
- { - let value = ReactEvent.Form.target(evt)["value"] - setInputValue(_ => value) - }} - /> - - - -
-
-
- - // Typography Demo - - - - {React.string("Typography System")} - - - -
- - {React.string("Display Text")} - - - {React.string("Heading Text")} - - - {React.string("Body text with proper line height and spacing for optimal readability.")} - - - {React.string("Caption text for additional information")} - -
-
-
-
-
-
- } -} - module App = { @react.component let make = () => {
- - - - -
} diff --git a/src/Router.res b/src/Router.res new file mode 100644 index 0000000..d76e11a --- /dev/null +++ b/src/Router.res @@ -0,0 +1,43 @@ +open Emotion.Css + +module NotFound = { + @react.component + let make = () => { + let containerStyles = css({ + "padding": "4rem 1rem", + "maxWidth": "600px", + "margin": "0 auto", + "textAlign": "center", + }) + +
+ + {React.string("404")} + + + {React.string("Page not found.")} + +
+ RescriptReactRouter.push("/")}> + {React.string("Go Home")} + +
+
+ } +} + +@react.component +let make = () => { + let url = RescriptReactRouter.useUrl() + + switch url.path { + | list{} => + | list{"components"} => + | list{"posts"} => + | list{"login"} => + | _ => + } +} diff --git a/src/components/NavBar/NavBar.res b/src/components/NavBar/NavBar.res new file mode 100644 index 0000000..dd4cece --- /dev/null +++ b/src/components/NavBar/NavBar.res @@ -0,0 +1,88 @@ +open Emotion.Css +open Emotion.Utils + +module NavBar = { + @react.component + let make = () => { + let url = RescriptReactRouter.useUrl() + let currentPath = switch url.path { + | list{} => "/" + | list{segment, ..._} => "/" ++ segment + } + + let navStyles = css({ + "display": "flex", + "alignItems": "center", + "justifyContent": "space-between", + "padding": "1rem 2rem", + "borderBottom": `1px solid ${Color.border}`, + "backgroundColor": Color.bgPrimary, + "position": "sticky", + "top": "0", + "zIndex": "100", + }) + + let brandStyles = css({ + "fontSize": "1.25rem", + "fontWeight": "700", + "color": Color.textPrimary, + "cursor": "pointer", + "textDecoration": "none", + }) + + let linksStyles = css({ + "display": "flex", + "gap": "1.5rem", + "alignItems": "center", + }) + + let linkStyles = (~active) => + css({ + "color": active ? Color.primary : Color.textSecondary, + "textDecoration": "none", + "fontWeight": active ? "600" : "400", + "fontSize": "0.9375rem", + "cursor": "pointer", + "transition": "color 200ms ease-out", + "borderBottom": active ? `2px solid ${Color.primary}` : "2px solid transparent", + "paddingBottom": "0.25rem", + }) + + let handleNav = (path, evt: ReactEvent.Mouse.t) => { + evt->ReactEvent.Mouse.preventDefault + RescriptReactRouter.push(path) + } + + + } +} diff --git a/src/pages/ComponentsPage.res b/src/pages/ComponentsPage.res new file mode 100644 index 0000000..4812866 --- /dev/null +++ b/src/pages/ComponentsPage.res @@ -0,0 +1,120 @@ +open Emotion.Css + +module Components = { + module Button = Button.Button + module Input = Input.Input + module Card = Card.Card + module Typography = Typography.Typography +} + +@react.component +let make = () => { + let (inputValue, setInputValue) = React.useState(() => "") + + let sectionStyles = css({ + "padding": "4rem 1rem", + "maxWidth": "1200px", + "margin": "0 auto", + }) + + let demoGridStyles = css({ + "display": "grid", + "gridTemplateColumns": "repeat(auto-fit, minmax(350px, 1fr))", + "gap": "2rem", + "marginTop": "3rem", + }) + +
+ + {React.string("Component Library")} + + + {React.string("Explore our comprehensive component library with variants, sizes, and states")} + +
+ + + + {React.string("Button Variants")} + + + +
+ + {React.string("Primary")} + + + {React.string("Secondary")} + + + {React.string("Outline")} + + + {React.string("Ghost")} + + + {React.string("Danger")} + +
+
+
+ + + + {React.string("Input Variants")} + + + +
+ { + let value = ReactEvent.Form.target(evt)["value"] + setInputValue(_ => value) + }} + /> + + + +
+
+
+ + + + {React.string("Typography System")} + + + +
+ + {React.string("Display Text")} + + + {React.string("Heading Text")} + + + {React.string( + "Body text with proper line height and spacing for optimal readability.", + )} + + + {React.string("Caption text for additional information")} + +
+
+
+
+
+} diff --git a/src/pages/HomePage.res b/src/pages/HomePage.res new file mode 100644 index 0000000..49e13cb --- /dev/null +++ b/src/pages/HomePage.res @@ -0,0 +1,127 @@ +open Emotion.Css + +module Components = { + module Button = Button.Button + module Typography = Typography.Typography + module Card = Card.Card +} + +module Logo = { + @module("../assets/react.svg") @react.component + external make: (~role: string=?, ~className: string=?, ~alt: string=?, unit) => React.element = + "default" +} + +module HeroSection = { + @react.component + let make = () => { + let (count, setCount) = React.useState(() => 0) + + let heroStyles = css({ + "minHeight": "80vh", + "display": "flex", + "flexDirection": "column", + "alignItems": "center", + "justifyContent": "center", + "textAlign": "center", + "padding": "2rem 1rem", + "background": `linear-gradient(180deg, ${Theme.Theme.Colors.brand["primary"]}0a 0%, transparent 100%)`, + }) + + let logoContainerStyles = css({ + "display": "flex", + "gap": "2rem", + "marginBottom": "2rem", + "alignItems": "center", + }) + +
+
+ + + +
+ + {React.string("Modern React Development")} + + + {React.string("Built with Rspack, ReScript, React, and Bun for lightning-fast development")} + +
+ setCount(prev => prev + 1)}> + {React.string("Interactive Demo (" ++ Int.toString(count) ++ ")")} + + + {React.string("View Components")} + +
+
+ } +} + +module FeaturesSection = { + @react.component + let make = () => { + let sectionStyles = css({ + "padding": "4rem 1rem", + "maxWidth": "1200px", + "margin": "0 auto", + }) + + let gridStyles = css({ + "display": "grid", + "gridTemplateColumns": "repeat(auto-fit, minmax(300px, 1fr))", + "gap": "2rem", + "marginTop": "3rem", + }) + + let features = [ + ("Lightning Fast", "Built with Rspack for ultra-fast bundling and hot reload"), + ("Type Safety", "ReScript provides bulletproof type safety and excellent ergonomics"), + ("Design System", "Comprehensive component library with consistent styling"), + ("Modern Stack", "Latest React, Emotion CSS-in-JS, and Bun package manager"), + ] + +
+ + {React.string("Why This Template?")} + +
+ {features + ->Belt.Array.map(((title, description)) => { + + + {React.string(title)} + + + {React.string(description)} + + + }) + ->React.array} +
+
+ } +} + +@react.component +let make = () => { +
+ + +
+} diff --git a/src/pages/LoginPage.res b/src/pages/LoginPage.res new file mode 100644 index 0000000..f9abdc2 --- /dev/null +++ b/src/pages/LoginPage.res @@ -0,0 +1,21 @@ +open Emotion.Css + +module Typography = Typography.Typography + +@react.component +let make = () => { + let containerStyles = css({ + "padding": "4rem 1rem", + "maxWidth": "400px", + "margin": "0 auto", + "textAlign": "center", + }) + +
+ {React.string("Sign In")} + + {React.string("Authentication with OAuth PKCE will be added here.")} + +
+} diff --git a/src/pages/PostsPage.res b/src/pages/PostsPage.res new file mode 100644 index 0000000..72f9b89 --- /dev/null +++ b/src/pages/PostsPage.res @@ -0,0 +1,21 @@ +open Emotion.Css + +module Typography = Typography.Typography + +@react.component +let make = () => { + let containerStyles = css({ + "padding": "4rem 1rem", + "maxWidth": "800px", + "margin": "0 auto", + "textAlign": "center", + }) + +
+ {React.string("Posts")} + + {React.string("Data fetching with Relay will be added here.")} + +
+}