diff --git a/deps.edn b/deps.edn index ca6be01..5fcd855 100644 --- a/deps.edn +++ b/deps.edn @@ -1,7 +1,6 @@ {:deps {org.clojure/clojure {:mvn/version "1.11.1"} - hiccup/hiccup {:mvn/version "1.0.5"} - hawk/hawk {:mvn/version "0.2.11"} - ring/ring-core {:mvn/version "1.9.6"} - ring/ring-jetty-adapter {:mvn/version "1.9.6"}} + hiccup/hiccup {:mvn/version "1.0.5"}} :aliases {:build {:main-opts ["-m" "ferano-io.core"]} - :dev {:main-opts ["-m" "ferano-io.dev"]}}} + :dev {:extra-deps {nrepl/nrepl {:mvn/version "1.0.0"} + cider/cider-nrepl {:mvn/version "0.28.5"}} + :main-opts ["-e" "(require 'ferano-io.core) (in-ns 'ferano-io.core)" "-r"]}}} diff --git a/dev-browser.sh b/dev-browser.sh index 1157284..5b014e2 100755 --- a/dev-browser.sh +++ b/dev-browser.sh @@ -5,19 +5,20 @@ sleep 1 BRAVE_PORT=9222 brave-browser --new-window --user-data-dir=/tmp/brave-dev --remote-debugging-port=${BRAVE_PORT} & +BRAVE_PID=$! echo "Wait 2 seconds, then go..." sleep 2 TAB_ID=$(curl -s http://localhost:${BRAVE_PORT}/json | jq -r '.[] | .id') echo "Got Tab ID: $TAB_ID" -JETTY_PORT=$(grep ":port" src/ferano_io/dev.clj | grep -o "[[:digit:]]\+") -echo "Found Jetty Port: $JETTY_PORT" +HTTP_PORT=$(grep "python -m http.server 3000" watch.sh | grep -o -e "[[:digit:]]\+") +echo "Found Jetty Port: $HTTP_PORT" JS_NAV=$(cat < .tab-id -echo "Open http://localhost:${JETTY_PORT}" +echo "Open http://localhost:${HTTP_PORT}" echo $JS_NAV | websocat ws://localhost:9222/devtools/page/${TAB_ID} > /dev/null diff --git a/resources/data.edn b/resources/data.edn index 64897aa..d53ead5 100644 --- a/resources/data.edn +++ b/resources/data.edn @@ -1,45 +1,80 @@ -{:personal {:name "Joseph Ferano" +{:sections {:about "ABOUT" :work "TITLES" :projects "PROJECTS" :contact "CONTACT" + ;; :demos "EXPERIMENTS" :blog "BLOG" + } + + :personal {:name "Joseph Ferano" :title "Software Engineer" :email "joseph@ferano.io" :location "Your City, State" :bio "Brief description of yourself and what you do."} - + + :projects [{:name "pmme" + :description "Done IoT air quality monitoring ecosystem with ESP32 sensors, cloud backend, mobile app, and web dashboard for real-time PM2.5 tracking" + :url "https://git.ferano.io/JosephFerano/pmme" + :technologies ["Rust" "ESP32" "ClojureScript" "Tauri" "Rocket" "PostgreSQL"] + :status "Active" + :year 2025} + {:name "8086 CPU Simulator" + :description "Done 8086 processor emulator with instruction decoding, cycle-accurate execution, and comprehensive testing suite" + :url "https://git.ferano.io/JosephFerano/performance-aware" + :technologies ["Odin" "Assembly" "x86"] + :status "Done" + :year 2024} + {:name "kanban-tui" + :description "Lightweight terminal-based Kanban tool with vim-like keybindings, built in Rust with SQLite persistence" + :url "https://git.ferano.io/JosephFerano/kanban-tui" + :technologies ["Rust" "ratatui" "SQLite"] + :status "Done" + :year 2023} + {:name "pydoku" + :description "Sudoku game with vim-style navigation and advanced Snyder notation pencil marking system for serious puzzle solving" + :url "https://git.ferano.io/JosephFerano/pydoku" + :technologies ["Python" "Pygame"] + :status "Complete" + :year 2023} + {:name "basicterm" + :description "Minimalist Linux terminal emulator written in C with SDL2, featuring ANSI escape sequence parsing and pseudoterminal communication" + :url "https://git.ferano.io/JosephFerano/basicterm" + :technologies ["C" "SDL2" "Linux"] + :status "Dormant" + :year 2022} + {:name "distributed-fs" + :description "Distributed file system with client-server architecture, featuring chunked storage across multiple data nodes and SQLite metadata tracking" + :url "https://git.ferano.io/JosephFerano/distributed-fs" + :technologies ["Rust" "SQLite" "TCP" "JSON"] + :status "Done" + :year 2018}] + :games [{:name "Wargate Heroes" :description "Mobile Multiplayer Online Battle Arena (MOBA)" - :image "./images/wargate.jpg" + :image "./static/images/wargate.jpg" :technologies ["Unity" "C#" "uLink" "Mobile"] :links [{:title "Trailer" :url "https://www.youtube.com/watch?v=kUcSleqOLCA"} {:title "Gameplay" :url "https://www.youtube.com/watch?v=RLPWUH4RKd8"}]} {:name "Jaws.io" :description "Online IO game where you play as the legendary shark!" - :image "./images/jaws.jpg" + :image "./static/images/jaws.jpg" :technologies ["Unity" "C#" "Photon" "Mobile"] :links [{:title "Trailer" :url "https://www.youtube.com/watch?v=uWEXo278UmQ"} {:title "Gameplay" :url "https://www.youtube.com/watch?v=LGnM5aAwFHw"}]} {:name "BreachTD" :description "Mobile Online Multiplayer Tower Defense" - :image "./images/breachtd.jpg" + :image "./static/images/breachtd.jpg" :technologies ["Unity" "C#" "uLink" "Mobile"] :links [{:title "Trailer" :url "https://www.youtube.com/watch?v=8wfprDgj_1Y"} {:title "Gameplay" :url "https://www.youtube.com/watch?v=QulMTsbj-Xk"}]} {:name "Startship Troopers: Invasion Mobile Infantry" :description "Cover shooter with runner mechanics and cinematics" - :image "./images/starship-troopers.jpg" + :image "./static/images/starship-troopers.jpg" :technologies ["Unity" "C#" "iOS"] :links [{:title "Trailer" :url "https://www.youtube.com/watch?v=ZPdTqhgWhaQ"} {:title "Gameplay" :url "https://www.youtube.com/watch?v=T5DXcvSaR-w"}]}] - - :projects [{:name "Project Name" - :description "Brief project description" - :url "https://github.com/user/project" - :demo "https://project-demo.com" - :technologies ["Clojure" "ClojureScript"]}] - + :experiments [{:name "Experiment Title" :description "Brief description of what this demonstrates" :url "https://codepen.io/user/experiment" :technologies ["JavaScript" "Canvas API"]}] - + :social {:github "https://github.com/username" :linkedin "https://linkedin.com/in/username" :twitter "https://twitter.com/username"}} diff --git a/src/ferano_io/core.clj b/src/ferano_io/core.clj index 43da0cd..abd41a7 100644 --- a/src/ferano_io/core.clj +++ b/src/ferano_io/core.clj @@ -3,24 +3,33 @@ [hiccup.page :as page] [clojure.edn :as edn])) -(def sections {:about "ABOUT" :work "TITLES" :projects "PROJECTS" - :demos "EXPERIMENTS" :blog "BLOG" :contact "CONTACT" }) - -(defn header [] +(defn header [data] [:header {:class "sticky top-0 bg-white shadow-sm z-50"} [:div {:class "max-w-6xl mx-auto px-4 py-4 flex justify-between items-center"} [:h1 {:class "text-2xl font-bold"} "Joseph Ferano"] [:nav {:class "hidden md:block"} [:ul {:class "flex space-x-8"} - (for [[id label] sections] + (for [[id label] (:sections data)] [:li [:a {:href (str "#" (name id)) :class "hover:text-blue-600 transition-colors"} label]])]] ;; Mobile hamburger (add JS later) [:button {:class "md:hidden p-2"} "☰"]]]) +(defn hero [data] + [:section {:id "hero" :class "bg-gray-900 py-8 md:py-16"} + [:div {:class "container mx-auto px-4 md:px-8"} + [:div {:class "flex flex-col md:flex-row items-center max-w-6xl mx-auto gap-6 md:gap-12"} + [:img {:src "/static/images/slide-01_resized.jpg" :alt "Hero image" + :class "rounded-lg shadow-lg w-full md:w-auto flex-shrink-0" + :style "width: 100%; max-width: 500px; object-fit: cover;"}] + [:div {:class "text-white flex-1 px-4 text-center md:text-left"} + [:h2 {:class "text-2xl md:text-4xl font-bold mb-3"} "Versatile Software Engineer & Game Programmer"] + [:p {:class "text-base md:text-lg opacity-90"} + "Building engaging experiences and robust systems across gaming, graphics, blockchain, and web technologies. Multilingual programmer with a diverse technical toolkit for solving complex problems."]]]]]) + (defn game-card [game] [:div {:class "bg-gray-50 border border-gray-200 p-5 flex flex-col md:flex-row gap-4 md:gap-8 md:items-center hover:shadow-md transition-shadow cursor-pointer"} [:div {:class "flex-shrink-0 w-full md:w-80"} - [:img {:src (:image game) + [:img {:src (:image game) :alt (:name game) :class "w-full h-48 md:h-full object-cover"}]] [:div {:class "flex-1 flex flex-col gap-4"} @@ -41,6 +50,26 @@ [:div {:class "grid grid-cols-1 gap-10 max-w-4xl mx-auto"} (map game-card (:games data))]]) +(defn projects-grid [data] + [:section {:id "projects" :class "py-16"} + [:h2 {:class "text-lg tracking-wide mb-8 text-center"} "PROJECTS"] + [:div {:class "max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"} + (for [project (:projects data)] + [:div {:class "bg-white border border-gray-200 p-5 hover:shadow-md transition-shadow"} + [:div {:class "flex justify-between items-start mb-3"} + [:h3 {:class "font-medium text-lg"} (:name project)] + [:span {:class "text-xs text-gray-500"} (:year project)]] + [:p {:class "text-sm text-gray-600 mb-4 line-clamp-3"} (:description project)] + [:div {:class "flex flex-wrap gap-1 mb-4"} + (for [tech (:technologies project)] + [:span {:class "text-xs px-2 py-1 bg-gray-100 rounded"} tech])] + [:div {:class "flex justify-between items-center"} + [:div {:class "flex gap-3"} + [:a {:href (:url project) :target "_blank" :class "text-xs text-blue-600 hover:underline"} "Source"] + (when (:demo project) + [:a {:href (:demo project) :target "_blank" :class "text-xs text-blue-600 hover:underline"} "Demo"])] + [:span {:class "text-xs px-2 py-1 bg-gray-100 rounded"} (:status project)]]])]]) + (defn main-page [data] (page/html5 [:head @@ -50,9 +79,10 @@ [:script {:src "https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"}]] [:body [:div {:class "min-h-screen bg-gray-50"} - (header) - [:main {:class "max-w-6xl mx-auto px-4 py-8"} - (games data)]]])) + (header data) + (hero data) + (projects-grid data) + (games data)]])) (defn -main [& args] (let [data (-> "resources/data.edn" diff --git a/src/ferano_io/dev.clj b/src/ferano_io/dev.clj deleted file mode 100644 index 9ea59f6..0000000 --- a/src/ferano_io/dev.clj +++ /dev/null @@ -1,44 +0,0 @@ -(ns ferano-io.dev - (:require [ferano-io.core :as core] - [hawk.core :as hawk] - [ring.adapter.jetty :as jetty] - [ring.util.response :as response] - [clojure.java.io :as io]) - (:import [java.io File])) - -(defn copy-static-files [] - (let [static-dir (io/file "resources/static") - build-dir (io/file "build")] - (io/make-parents build-dir) - (when (.exists static-dir) - (doseq [file (file-seq static-dir) - :when (.isFile file)] - (let [rel-path (subs (.getPath file) (inc (count (.getPath static-dir)))) - target (io/file build-dir rel-path)] - (io/make-parents target) - (io/copy file target)))))) - -(defn rebuild! [] - (println "Rebuilding...") - (require 'ferano-io.core :reload) - (copy-static-files) - (core/-main) - (println "Done.")) - -(defn handler [request] - (let [path (str "build" (:uri request)) - file (File. (if (.endsWith path "/") (str path "index.html") path))] - (if (.exists file) - (response/file-response (.getPath file)) - (response/not-found "Not found")))) - -(defn -main [& args] - (rebuild!) - (hawk/watch! [{:paths ["src" "resources"] - :handler (fn [ctx e] - (println "File changed:" (:file e)) - (rebuild!))}]) - (jetty/run-jetty handler {:port 3000 :join? false}) - (println "Server running on http://localhost:3000") - (println "Watching for changes...") - @(promise)) ; Keep alive diff --git a/watch.sh b/watch.sh new file mode 100755 index 0000000..2afc557 --- /dev/null +++ b/watch.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +python -m http.server 3000 --directory build & +PID_HTTP=$! + +sleep 2 +if ! kill -0 $PID_HTTP 2>/dev/null; then + echo "Failed to start HTTP server" + exit 1 +fi + +# When using the -d flag, entr will exist, so just keep looping with a function to get it to relaunch +watch_static() { + while true; do + echo resources/static/ | entr -dp rsync -av --delete resources/static/ build/static/ + sleep 0.5 + done +} + +watch_static & +PID_WATCH_STATIC=$! + +cleanup() { + echo "Shutting down..." + kill $PID_HTTP 2>/dev/null + kill $PID_WATCH_STATIC 2>/dev/null + exit 0 +} + +trap cleanup SIGINT SIGTERM + +find src/ resources/ -name "*.clj" -o -name "*.edn" | entr -r clj -M:build + +cleanup