Compare commits

...

10 Commits

Author SHA1 Message Date
35d279618a swap output path 2025-07-31 19:45:16 -05:00
98c98b843b add notes 2025-07-31 19:45:00 -05:00
7caccd764f add browser? 2025-07-31 19:44:52 -05:00
45c8cecc34 add server testing 2025-07-31 19:44:31 -05:00
G
e9ab029584 update .gitignore 2025-03-09 05:44:00 -05:00
G
e1b7c5acc0 add main.css 2025-03-09 05:43:50 -05:00
G
36055e8a02 add index.html 2025-03-09 05:43:41 -05:00
G
da9b1fb550 rmv 2025-03-09 05:43:25 -05:00
G
32c3e7259f update deps 2025-03-09 05:43:20 -05:00
G
6f5fc74aaa rmv 2025-03-09 05:43:11 -05:00
10 changed files with 202 additions and 272 deletions

4
.gitignore vendored
View File

@@ -1,7 +1,7 @@
/public/js
/resources/js
/node_modules
/target
/.shadow-cljs
/*.iml
/.nrepl-port
/.idea
/.idea

View File

@@ -126,3 +126,24 @@ Use `CTRL+C` to stop the `watch` process and instead run `npx shadow-cljs releas
When done you can open `http://localhost:8020` and see the `release` build in action. At this point you would usually copy the `public` directory to the "production" web server.
Note that in the default config we overwrote the `public/js/main.js` created by the `watch`. You can also configure a different path to use for release builds but writing the output to the same file means we do not have to change the `index.html` and test everything as is.
export $(cat ./.env | grep -v ^# | xargs) >/dev/null
npx shadow-cljs watch app

View File

@@ -1,4 +0,0 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
color: green;
}

View File

@@ -4,11 +4,10 @@
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/main.css">
<title>Browser Starter</title>
<link rel="stylesheet" href="/main.css">
<title>Mardika</title>
</head>
<body>
<h1>shadow-cljs - Browser</h1>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="app"></div>
<script src="/js/main.js"></script>

View File

@@ -21,10 +21,10 @@ body {
.overlay {
text-align: center;
position: absolute;
bottom: 8%;
bottom: 1%;
left: 0;
width: 100%;
height: 50%;
height: 40%;
background-color: rgba(255, 255, 255, 0.8);
z-index: 100;
}

View File

@@ -5,15 +5,16 @@
"src/test"]
:dependencies
[[reagent "1.1.1"] [re-frame "1.4.3"] [cljs-ajax "0.8.1"] [environ "1.2.0"] [adzerk/env "0.4.0"]]
[[reagent "1.1.1"] [re-frame "1.4.3"] [cljs-ajax "0.8.1"]
]
:build-hooks [(shadow-env.core/hook)]
:dev-http
{8020 "public"}
{8020 "resources"}
:builds
{:app
{:target :browser
:output-dir "public/js"
:output-dir "resources/js"
:asset-path "/js"
:closures-defines {
"process.env.API_KEY" (System/getenv "API_KEY")
@@ -22,4 +23,4 @@
}
:modules
{:main ; becomes public/js/main.js
{:init-fn starter.browser/init}}}}}
{:init-fn browser/main}}}}}

View File

@@ -1,19 +1,83 @@
(ns server
(:require [ring.middleware.resource :refer [wrap-resource]]
[clojure.core.async :refer [go]] [clojure.java.io :as io]
[ring.middleware.cors :refer [wrap-cors]] [clojure.string :as str]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]
[compojure.core :refer [routes defroutes GET POST]]
[org.httpkit.server :as server]))
[compojure.core :refer [routes defroutes GET POST]] [clj-http.client :as client]
[org.httpkit.server :as server])
(:import [java.io FileOutputStream]))
(defn download-file [url destination]
(println "Downloading from:" url)
(let [content (slurp url)
response (client/get url)]
;; Ensure parent directories exist
(io/make-parents destination)
;; Save the content to the destination file
(spit destination content)
{:status 200
:headers {"Content-Type" (get-in response [:headers "content-type"] "text/plain")}
:body (:body response)}))
(defn proxy-url [url]
(if url
(try
(let [response (client/get url)]
{:status 200
:headers {"Content-Type" (get-in response [:headers "content-type"] "text/plain")
"Access-Control-Allow-Origin" "*"
"User-Agent" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0"
"Access-Control-Expose-Headers" "*"
"X-Final-Destination" url
"X-Set-Cookie" (get-in response [:headers "set-cookie"] "")
"Vary" "Origin"
"Redirect" "follow"}
:body (:body response)})
(catch Exception e
{:status 500
:headers {"Content-Type" "application/json"}
:body (str "{\"error\": \"" (.getMessage e) "\"}")}))
{:status 400
:headers {"Content-Type" "application/json"}
:body "{\"error\": \"Missing 'url' query parameter\"}"}))
(defroutes app-routes
(GET "/api/data" [] {:status 200 :headers {"Content-Type" "application/json"} :body "{\"message\": \"Something I guess\"}"})
(GET "/" [] (slurp (clojure.java.io/resource "index.html")))
(GET "/api" {query-params :query-params}
(if (not= nil query-params)
(do
(println "Has /api so we will use the query param \n\n")
(let [url (get query-params "url")]
(proxy-url url)))
)
)
#_(GET "/player" [] {:status 200
:headers {"Content-Type" (get-in response [:headers "content-type"] "text/plain")}
:body (:body response)})
(POST "/api/data" [data] {:status 200 :headers {"Content-Type" "application/json"} :body (str "{\"received\": \"" data "\"}")})
(GET "*" [] {:status 404 :headers {"Content-Type" "text/plain"} :body "Not Found"}))
(GET "/*" {uri :uri}
(if (not (str/starts-with? uri "/api"))
(do
(println "Not /api so we are going to use the URI \n\n")
(println uri)
(download-file (str "https://player.vidbinge.com" uri) (str "resources" uri))))
)
#_(GET "*" [] {:status 404 :headers {"Content-Type" "text/plain"} :body "Not Found"}))
(def app
(-> (routes app-routes)
(wrap-resource "")
(wrap-defaults site-defaults)))
(wrap-defaults site-defaults)
(wrap-cors :access-control-allow-origin [#".*"]
:access-control-allow-methods [:get :post :put :delete :options]
:access-control-allow-headers ["Content-Type"])))
(defn -main []
(println "Server running at http://localhost:8080")
(server/run-server app {:port 8080}))
(println "Server running at http://localhost:3030")
(server/run-server app {:port 3030}))

View File

@@ -2,10 +2,33 @@
(:require [re-frame.core :as rf]
[reagent.core :as r]
[reagent.dom :as dom]
[ajax.core :refer [GET json-response-format]]
[cljs.core.async :refer [chan timeout put! <! go-loop]]
[ajax.core :refer [GET raw-response-format json-response-format]]
[cljs.core.async :refer [chan timeout put! go <! go-loop]]
[env :refer [env]]))
(defn temp-url [url callback]
(let [c (chan)]
(println "Fetching from URL:" url)
(GET url
{:response-format (raw-response-format)
:handler #(do
(println "Received response:" (js->clj %))
(put! c %))
:error-handler #(do
(js/console.error "Error occurred:" %)
(put! c nil))})
(go-loop []
(let [response (<! c)]
(println "processing " response)
(if (nil? response)
(do
(println "Response is nil, retrying in 1 second...")
(<! (timeout 1000)) ; Wait for 1 second before retrying
(recur)) ; Retry
)
(callback response)))))
(defn get-url [url]
(let [c (chan)]
(println "Fetching from URL:" url)
@@ -82,8 +105,8 @@
(rf/reg-event-db
:search-movies
(fn [db [_ query]]
(let [url (str (env :base-url) "/search/movie?api_key=" (env :api-key) "&query=" query)]
(println "Searching movies for query:" query "with URL:" url)
(let [url (str (env :base-url) "/search/multi?api_key=" (env :api-key) "&query=" query)]
(println "Searching for query:" query "with URL:" url)
(get-url url)
(when-not (:loading? db)
{:db (assoc db :loading? true)
@@ -92,27 +115,72 @@
(defn search-bar []
(let [query (r/atom "")]
(fn []
[:div
[:div {:style {:display "flex"
:align-items "center"
:justify-content "center"
:margin-top "50px"}}
[:input {:type "text"
:class "search-input wide"
:placeholder "Search for title..."
:value @query
:on-change #(reset! query (-> % .-target .-value))}]
[:button {:on-click #(rf/dispatch [:search-movies @query])} "Search"]])))
:on-change #(reset! query (-> % .-target .-value))
:on-key-down #(when (= (.-key %) "Enter")
(rf/dispatch [:search-movies @query]))}]
[:button {:class "search-button small"
:on-click #(rf/dispatch [:search-movies @query])} "Search"]])))
(defn vid-src-handle [props]
(let [tv? (= "tv" (:media_type props))]
(str (env :vid-src) (if tv? "tv" "movie") "/" (:id props) (if tv? "/1/1" ""))))
(rf/reg-sub
:provider
(fn [db _]
(:provider db)))
(rf/reg-event-db
:load-provider
(fn [db [_ provider]]
(assoc db :provider provider)))
(def iframe-ref (r/atom nil))
(defn expanded-interface [props]
[:div
{:style {:position "fixed" :top 0 :left 0 :width "100%" :height "100%" :background-color "white" :z-index 1000 :overflow "auto"}}
[:button {:on-click #(rf/dispatch [:collapse-item])} "Close"]
[:h3 (:title props)]
[:iframe {:allowFullScreen true
:autoPlay true
:src (vid-src-handle props)
:style {:width "100%"
:height "85%"}}]])
(let [vidya @(rf/subscribe [:provider])]
[:div
{:style {:position "fixed"
:bottom 0
:left 0
:width "100%"
:height "100%"
:background-color "black"
:z-index 1000
:overflow "auto"}}
[:button.close {:on-click #(rf/dispatch [:collapse-item])} "Back"]
[:iframe {:allowFullScreen true
:autoPlay true
:src (vid-src-handle props)
:ref #(reset! iframe-ref %)
:style {:width "100%"
:height "100%"
:z-index 1000}
:on-load #((go (<! (timeout 8000 ))
(js/console.log (.-documentElement js/document))))}]
#_(when vidya
[:div {:dangerouslySetInnerHTML {:__html vidya}}])]))
#_[ [:div.vidya
{:dangerouslySetInnerHTML {:__html (or @html-content "Loading...")}}]] ; Inject raw HTML content]
#_[:iframe {:allowFullScreen true
:autoPlay true
:src (vid-src-handle props)
:style {:width "100%"
:height "95%"
:z-index 1000}}]
(rf/reg-sub
:expanded-item
@@ -129,6 +197,8 @@
(fn [db _]
(dissoc db :expanded-item)))
;; Subscriptions
(rf/reg-sub
:items
@@ -159,8 +229,13 @@
;; Show hover interface
(if @hovered
[:div
{:on-click #((println (:id props))
(rf/dispatch [:expand-item (:id props)]))}
{:on-click
#_(fn [e] (set! (.-href js/window.location) "/api?url=https://vidbinge.dev/embed/movie/1104845"))
(do
#_(temp-url "http://localhost:8080/api?url=https://vidbinge.dev/embed/movie/1104845" (fn [e] (rf/dispatch [:load-provider e])))
#(rf/dispatch [:expand-item (:id props)]))}
;; Hover overlay content
[:div {:class "overlay"}
;; Title
@@ -183,9 +258,11 @@
[(with-hover item-component) item])])
(defn chunk-items [items chunk-size]
(map-indexed (fn [index chunk]
{:id index :items chunk})
(partition-all chunk-size items)))
(let [filtered-items (filter :poster_path items)]
(map-indexed (fn [index chunk]
{:id index :items chunk})
(partition-all chunk-size filtered-items))))
;; Components
(defn main-page []
@@ -204,7 +281,7 @@
loading? @(rf/subscribe [:loading?])]
(println "Rendering items:" items)
[:div
[:h1 "Endless Scrolling TMDB"]
[:h1 {:style {:text-align "center"}} "Mardika"]
[:div [search-bar]]
(for [chunk (chunk-items items 4)] ;; Chunk items into groups of 4
^{:key (:id chunk)} ;; Use the unique :id for the key

View File

@@ -1,223 +0,0 @@
(ns starter.browser
(:require [re-frame.core :as rf]
[reagent.core :as r]
[reagent.dom :as dom]
[ajax.core :refer [GET json-response-format]]
[cljs.core.async :refer [chan timeout put! <! go-loop]]
[starter.env :refer [env]]
))
(println env)
(defn get-url [url]
(let [c (chan)]
(js/console.log "Fetching movies for URL:" url)
(GET url
{:response-format (json-response-format {:keywords? true})
:handler #(do
(js/console.log "Received response:" (js->clj %))
(put! c %))
:error-handler #(do
(js/console.error "Error occurred:" %)
(put! c nil))})
(go-loop []
(let [response (<! c)]
(println "Processing: " (:results response))
(if (nil? response)
(do
(js/console.log "Response is nil, retrying in 1 second...")
(<! (timeout 1000)) ; Wait for 1 second before retrying
(recur)) ; Retry
(let [results (:results response)]
(js/console.log "Dispatching results:" results)
(rf/dispatch [:update-items results])))))))
(defn fetch-movies [page]
(let [url (str (env :base-url) "/movie/popular?api_key=" (env :api-key) "&page=" page)]
(js/console.log "Fetching movies for page" page "with URL:" url)
(get-url url)))
(rf/reg-event-db
:initialize
(fn [_ _]
(js/console.log "Initializing state")
(let [initial-state {:items [] :loading? true :page 1}]
(fetch-movies 1)
initial-state)))
(rf/reg-event-db
:update-items
(fn [db [_ new-items]]
(js/console.log "Updating items with:" new-items)
(if (seq new-items)
(-> db
(update :items concat new-items)
(update :page inc)
(assoc :loading? false))
(assoc db :loading? false))))
(rf/reg-event-db
:fetch-more-items
(fn [db _]
(let [page (:page db)]
(js/console.log "Dispatching fetch-more-items for page" page)
(fetch-movies page)
(assoc db :loading? true))))
;; Trigger fetch-more-items event on scroll
(rf/reg-event-fx
:scroll-handler
(fn [{:keys [db]} _]
(js/console.log "Scroll handler triggered")
(when (<= (- (.-scrollHeight (.-documentElement js/document))
(.-scrollTop (.-documentElement js/document))
(.-clientHeight (.-documentElement js/document))) 200) ; Increased threshold
(js/console.log "Almost at the bottom of the page")
(when-not (:loading? db)
{:db (assoc db :loading? true)
:dispatch [:fetch-more-items]}))))
;; Add scroll event listener
(defn handle-scroll []
(js/console.log "Scroll event detected")
(rf/dispatch [:scroll-handler]))
(rf/reg-event-db
:search-movies
(fn [db [_ query]]
(let [url (str (env :base-url) "/search/movie?api_key=" (env :api-key) "&query=" query)]
(js/console.log "Searching movies for query:" query "with URL:" url)
(get-url url)
(when-not (:loading? db)
{:db (assoc db :loading? true)
:dispatch [:fetch-more-items]}))))
(defn search-bar []
(let [query (r/atom "")]
(fn []
[:div
[:input {:type "text"
:placeholder "Search for a movie..."
:value @query
:on-change #(reset! query (-> % .-target .-value))}]
[:button {:on-click #(rf/dispatch [:search-movies @query])} "Search"]])))
(defn expanded-interface [props]
[:div
{:style {:position "fixed" :top 0 :left 0 :width "100%" :height "100%" :background-color "white" :z-index 1000 :overflow "auto"}}
[:button {:on-click #(rf/dispatch [:collapse-item])} "Close"]
[:h3 (:title props)]
[:p (:overview props)]
[:iframe {:allowFullScreen true
:autoPlay true
:src (str (env :vid-src) (:id props))
:style {:width "100%"
:height "85%"}}]])
(rf/reg-sub
:expanded-item
(fn [db _]
(println "Getting" (:expanded-item db))
(:expanded-item db)))
(rf/reg-event-db
:expand-item
(fn [db [_ item-id]]
(js/console.log "Setting expanded-item:" item-id)
(assoc db :expanded-item item-id)))
(rf/reg-event-db
:collapse-item
(fn [db _]
(dissoc db :expanded-item)))
;; Subscriptions
(rf/reg-sub
:items
(fn [db _]
(:items db)))
(rf/reg-sub
:loading?
(fn [db _]
(:loading? db)))
(defn with-hover [component]
(fn [props]
(let [hovered (r/atom false)
expanded-item @(rf/subscribe [:expanded-item])]
(fn [props]
(println expanded-item)
[:<>
;; Render the expanded interface if this item is expanded
(if (= (:id props) @(rf/subscribe [:expanded-item]))
[expanded-interface props]
;; Otherwise, handle hover logic and normal view
[:div.item {:on-mouse-enter #(reset! hovered true)
:on-mouse-leave #(reset! hovered false)
:style {:background-color (if @hovered "lightblue" "lightgray")
:position "relative"
:width "100%"
:height "100%"}}
;; Show hover interface
(if @hovered
[:div
{:on-click #((println (:id props))
(rf/dispatch [:expand-item (:id props)]))}
;; Hover overlay content
[:div {:class "overlay"}
;; Title
[:h3 (:title props)]
;; Description
[:p (:overview props)]]
[:img {:src (str "https://image.tmdb.org/t/p/w342" (:poster_path props))}]]
;; Default view for non-hovered state
[component props])])]))))
(defn item-component [props]
[:div.item
[:img {:src (str "https://image.tmdb.org/t/p/w342" (:poster_path props))
:id (str "cover-img" (:id props))}]])
(defn item-components [items]
[:div.items-container
(for [item items]
^{:key (:id item)}
[(with-hover item-component) item])])
(defn chunk-items [items chunk-size]
(map-indexed (fn [index chunk]
{:id index :items chunk})
(partition-all chunk-size items)))
;; Components
(defn main-page []
(r/create-class
{:component-did-mount
(fn []
(js/console.log "Component did mount")
(js/addEventListener "scroll" handle-scroll))
:component-will-unmount
(fn []
(js/console.log "Component will unmount")
(js/removeEventListener "scroll" handle-scroll))
:reagent-render
(fn []
(let [items @(rf/subscribe [:items])
loading? @(rf/subscribe [:loading?])]
(js/console.log "Rendering items:" items)
[:div
[:h1 "Endless Scrolling TMDB Movies"]
[:div [search-bar]]
(for [chunk (chunk-items items 4)] ;; Chunk items into groups of 4
^{:key (:id chunk)} ;; Use the unique :id for the key
[item-components (:items chunk)])
(when loading?
[:div "Loading..."])]))}))
(defn init []
(rf/dispatch-sync [:initialize])
(dom/render [main-page] (.getElementById js/document "app")))
;; Initialize app
(init)

View File

@@ -1,5 +0,0 @@
(ns starter.fake-env)
(def env {:api-key ""
:base-url ""
:vid-src ""})