First commit

This commit is contained in:
Joseph Ferano 2025-06-18 16:43:08 +07:00
commit e935d5b3db
15 changed files with 192 additions and 0 deletions

1
.dir-locals.el Normal file
View File

@ -0,0 +1 @@
((nil . ((joe/shell-command-silent-default . "./ferano.io/browser-reload.sh"))))

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.cpcache/
.nrepl-port
build/*
/.tab-id
\*Claude*

5
browser-reload.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/sh
TAB_PATH=$(dirname "$0")
TAB_ID=$(cat ${TAB_PATH}/.tab-id)
echo '{"id":1,"method":"Runtime.evaluate","params":{"expression":"location.reload()"}}' | websocat ws://localhost:9222/devtools/page/$TAB_ID > /dev/null

7
deps.edn Normal file
View File

@ -0,0 +1,7 @@
{: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"}}
:aliases {:build {:main-opts ["-m" "ferano-io.core"]}
:dev {:main-opts ["-m" "ferano-io.dev"]}}}

23
dev-browser.sh Executable file
View File

@ -0,0 +1,23 @@
#!/bin/sh
# Wait for DevTools to start listening on websocket
sleep 1
BRAVE_PORT=9222
brave-browser --new-window --user-data-dir=/tmp/brave-dev --remote-debugging-port=${BRAVE_PORT} &
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"
JS_NAV=$(cat <<EOF
{"id":1,"method":"Page.navigate","params":{"url":"http://localhost:${JETTY_PORT}"}}
EOF
)
echo $TAB_ID > .tab-id
echo "Open http://localhost:${JETTY_PORT}"
echo $JS_NAV | websocat ws://localhost:9222/devtools/page/${TAB_ID} > /dev/null

45
resources/data.edn Normal file
View File

@ -0,0 +1,45 @@
{:personal {:name "Joseph Ferano"
:title "Software Engineer"
:email "joseph@ferano.io"
:location "Your City, State"
:bio "Brief description of yourself and what you do."}
:games [{:name "Wargate Heroes"
:description "Mobile Multiplayer Online Battle Arena (MOBA)"
:image "./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"
: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"
: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"
: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"}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

62
src/ferano_io/core.clj Normal file
View File

@ -0,0 +1,62 @@
(ns ferano-io.core
(:require [hiccup.core :as h]
[hiccup.page :as page]
[clojure.edn :as edn]))
(def sections {:about "ABOUT" :work "TITLES" :projects "PROJECTS"
:demos "EXPERIMENTS" :blog "BLOG" :contact "CONTACT" })
(defn header []
[: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]
[: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 game-card [game]
[:div {:class "bg-gray-50 border border-gray-200 p-5 flex gap-8 items-center hover:shadow-md transition-shadow cursor-pointer"}
[:div {:class "flex-shrink-0 w-80"}
[:img {:src (:image game)
:alt (:name game)
:class "w-full h-full object-cover"}]]
[:div {:class "flex-1 flex flex-col gap-4"}
[:h3 {:class "text-xl font-normal"} (:name game)]
[:p {:class "text-sm text-gray-600 leading-relaxed"} (:description game)]
[:div {:class "text-xs text-gray-500 italic"}
(clojure.string/join ", " (:technologies game))]
[:div {:class "flex justify-center gap-4"}
(for [link (:links game)]
[:a {:href (:url link)
:target "_blank"
:class "text-xs text-gray-800 no-underline py-1 px-3 border border-gray-300 hover:bg-gray-100 transition-colors"}
(:title link)])]]])
(defn games [data]
[:section {:id "work" :class "py-5 pb-15"}
[:h2 {:class "text-lg tracking-wide mb-8 text-center"} "FEATURED GAMES"]
[:div {:class "grid grid-cols-1 gap-10 max-w-4xl mx-auto"}
(map game-card (:games data))]])
(defn main-page [data]
(page/html5
[:head
[:meta {:charset "utf-8"}]
[:meta {:name "viewport" :content "width=device-width, initial-scale=1"}]
[:title "Joseph Ferano"]
[: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)]]]))
(defn -main [& args]
(let [data (-> "resources/data.edn"
slurp
edn/read-string)]
(spit "build/index.html" (main-page data))
(println "Generated build/index.html")))

44
src/ferano_io/dev.clj Normal file
View File

@ -0,0 +1,44 @@
(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