Compare commits
No commits in common. "d894964c204a4b2762f82ff6e07f03d771d12a40" and "a9c48e59c951b5e5251d1b61fdcd2e33c597a174" have entirely different histories.
d894964c20
...
a9c48e59c9
|
|
@ -4,8 +4,7 @@
|
||||||
(:use :cl)
|
(:use :cl)
|
||||||
(:export :internal-disable-debugger)
|
(:export :internal-disable-debugger)
|
||||||
(:export :internal-quit
|
(:export :internal-quit
|
||||||
:pht
|
:pht))
|
||||||
:member-string))
|
|
||||||
|
|
||||||
(in-package :asteroid.app-utils)
|
(in-package :asteroid.app-utils)
|
||||||
|
|
||||||
|
|
@ -20,11 +19,6 @@
|
||||||
(internal-quit)))
|
(internal-quit)))
|
||||||
(setf *debugger-hook* #'internal-exit)))
|
(setf *debugger-hook* #'internal-exit)))
|
||||||
|
|
||||||
(defun member-string (item seq)
|
|
||||||
"Checkes if a string 'item' is a member of a list. Returns t or nil for the finding result."
|
|
||||||
(when (member item seq :test #'string-equal)
|
|
||||||
t))
|
|
||||||
|
|
||||||
(defun internal-quit (&optional code)
|
(defun internal-quit (&optional code)
|
||||||
"Taken from the cliki"
|
"Taken from the cliki"
|
||||||
;; This group from "clocc-port/ext.lisp"
|
;; This group from "clocc-port/ext.lisp"
|
||||||
|
|
|
||||||
|
|
@ -1009,7 +1009,6 @@
|
||||||
(error () 0))))
|
(error () 0))))
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(load-template "admin")
|
(load-template "admin")
|
||||||
:navbar-exclude '("admin")
|
|
||||||
:title "🎵 ASTEROID RADIO - Admin Dashboard"
|
:title "🎵 ASTEROID RADIO - Admin Dashboard"
|
||||||
:server-status "🟢 Running"
|
:server-status "🟢 Running"
|
||||||
:database-status (handler-case
|
:database-status (handler-case
|
||||||
|
|
@ -1028,7 +1027,6 @@
|
||||||
(require-authentication)
|
(require-authentication)
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(load-template "users")
|
(load-template "users")
|
||||||
:navbar-exclude '("profile" "users")
|
|
||||||
:title "ASTEROID RADIO - User Management"))
|
:title "ASTEROID RADIO - User Management"))
|
||||||
|
|
||||||
;; User Profile page (requires authentication)
|
;; User Profile page (requires authentication)
|
||||||
|
|
@ -1037,7 +1035,6 @@
|
||||||
(require-authentication)
|
(require-authentication)
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(load-template "profile")
|
(load-template "profile")
|
||||||
:navbar-exclude '("about" "status" "profile")
|
|
||||||
:title "🎧 admin - Profile | Asteroid Radio"
|
:title "🎧 admin - Profile | Asteroid Radio"
|
||||||
:username "admin"
|
:username "admin"
|
||||||
:user-role "admin"
|
:user-role "admin"
|
||||||
|
|
|
||||||
|
|
@ -1,160 +0,0 @@
|
||||||
#+TITLE: Templating System
|
|
||||||
#+AUTHOR: Asteroid Radio Development Team
|
|
||||||
#+DATE: 2026-01-17
|
|
||||||
|
|
||||||
* Introduction
|
|
||||||
|
|
||||||
The radiance ecosystem includes an HTML templating system that is built on top of [[https://quickdocs.org/clip][Clip]] and [[https://shinmera.github.io/lquery/][lquery]] for advanced costumization.
|
|
||||||
|
|
||||||
While =clip= enables conditional logic on rendering parts in an HTML document, re-using the same tag mechanics, =lquery= enables dynamically setting of element properties with some lisp integrations.
|
|
||||||
|
|
||||||
Dominating these tools enables some smarter approaches on page content building with conditional rendering and code reuse.
|
|
||||||
|
|
||||||
* Clip mechanics
|
|
||||||
|
|
||||||
** Conditionals
|
|
||||||
|
|
||||||
Clip uses HTML tags for its logic and has 3 simple conditional directives: =if=, =when= and =unless=, that work exactly as their lisp counterparts, having the =test= attribute as the validator for truthness.
|
|
||||||
|
|
||||||
The =if= check is accomplished by the =c:if= tag, and requires a =c:then= tag for the body of its branch. It also has =c:else= and =c:elseif= to enable multiple branching:
|
|
||||||
|
|
||||||
#+begin_src html
|
|
||||||
<c:if test='framesetp'>
|
|
||||||
<c:then>
|
|
||||||
<!-- HTML test true branch -->
|
|
||||||
</c:then>
|
|
||||||
<c:else>
|
|
||||||
<!-- HTML test false branch -->
|
|
||||||
</c:else>
|
|
||||||
</c:if>
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
The above example tests for the truthness of the variable =framesetp= and executes the associate branch based on that. The =test= argument can, naturally, receive any kind of lisp syntax.
|
|
||||||
|
|
||||||
The =c:when= and =c:unless= directives, like their lisp counterparts, only have a branch that is executed when the check of =test= argument value is true or false accordingly.
|
|
||||||
|
|
||||||
*Note:* Clip also has =c:case= and =c:cond= conditionals that should work like they lisp counterparts but I didn't need them until now so I'll not try to explain them.
|
|
||||||
|
|
||||||
** Data usage
|
|
||||||
|
|
||||||
Using a variable content in the template engine is accomplished by the =lquery= tag attribute, which will be explained later. But as this is a tag attribute, it requires an HTML tag to be attached. To enable the possibility of inserting something anywhere on the HTML document, Clip supplies the =c:splice= tag, a virtual HTML node that is removed after rendering, leaving only the content inserted by =lquery=.
|
|
||||||
|
|
||||||
The follow example inserts a textual value of the =content= variable directly into the HTML page:
|
|
||||||
|
|
||||||
#+begin_src html
|
|
||||||
<c:splice lquery='(text content)></c:splice>
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
However, a common use case is inserting external HTML rendered content in a different HTML document, for example, to reuse a navbar HTML partial in every page:
|
|
||||||
|
|
||||||
#+begin_src html
|
|
||||||
<c:splice lquery='(html navbar)></c:splice>
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
Those two usages are so frequently that Clip supplies shorhand aliases for both as the =c:s= and =c:h= tags:
|
|
||||||
|
|
||||||
#+begin_src html
|
|
||||||
<!-- Insert a textual value -->
|
|
||||||
<c:s>content</c:s>
|
|
||||||
|
|
||||||
<!-- Insert an HTML partial to render -->
|
|
||||||
<c:h>navbar</c:h>
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
A great usage of this feature is our new navbar reusable HTML partial, that can be loaded from a speficic lisp function on any page:
|
|
||||||
|
|
||||||
#+begin_src html
|
|
||||||
<c:h>(asteroid::load-template "partial/navbar")</c:h>
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
*Note:* As far as I understand, the templating engine works on a different package, so refering to the =asteroid= package is always required inside a template to use defined functions.
|
|
||||||
|
|
||||||
** Clipboard environments
|
|
||||||
|
|
||||||
Clip uses the concept of environment as the data that is available to be used in the templating system.
|
|
||||||
|
|
||||||
The =c:let= directive lets define a new clipboard environment directly on the template. This will supersede the default clipboard, so any useful data needs to also be added to the clipboard for easy access:
|
|
||||||
|
|
||||||
#+begin_src html
|
|
||||||
|
|
||||||
<c:let new-var='"hello"' forward='var'>
|
|
||||||
<!-- HTML content -->
|
|
||||||
</c:let>
|
|
||||||
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
The above example defines a local =new-var= with the value ="hello"= and a local =forward= with the parent =var= variable content.
|
|
||||||
|
|
||||||
Alternatively, if forwarding a variable to the new clipboard environment is not desired, the special =(** :var)= syntax can be used to access the parent clipboard.
|
|
||||||
|
|
||||||
The =c:using= directive creates a new clipboard environment with the contents of an existing variable on the parent clipboard.
|
|
||||||
|
|
||||||
#+begin_src html
|
|
||||||
|
|
||||||
<c:using value='obj'>
|
|
||||||
<!-- HTML content using the 'obj.attribute1' value-->
|
|
||||||
<c:splice>attribute1</c:splice>
|
|
||||||
</c:let>
|
|
||||||
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
The example creates a new clipboard environment inside the =c:using= attribute that has direct access to all properties of some structured variable =obj=. This tag transparently destructures any kind of composed data structure like =plists=, =alists= and =classes=.
|
|
||||||
|
|
||||||
Like the simplicity of structural access of the =c:using= tag, the =clip= function enables the same easy of access in any template context. For example, the same splice of the previous example can be obtained with:
|
|
||||||
|
|
||||||
#+begin_src html
|
|
||||||
|
|
||||||
<c:s>(clip obj :attribute1)</c:s>
|
|
||||||
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
This approach is, clearly, more useful when a single value is wanted, while the previous when several values on the structured variable will be used.
|
|
||||||
|
|
||||||
** Iterators
|
|
||||||
|
|
||||||
Clip also has the =c:iterate= directive that iterates a sequence, and its body is rendered having the clipboard environment matching each element of that iteration. As I am yet to use this directive, I will leave its documentation to a later moment.
|
|
||||||
|
|
||||||
* lquery mechanics
|
|
||||||
|
|
||||||
The [[https://shinmera.github.io/lquery/][lquery]] library enables changing any property of an HTML node, being its =text= or =html= content, =css= styles, element attributes (=attr=) and assigned css classes.
|
|
||||||
|
|
||||||
Its usage is very simple: an =lquery= attribute is added to an HTML document, and its value is a s-expression with an accepted funtion and the value for it to work on.
|
|
||||||
|
|
||||||
The following topics present some basic accepted =lquery= functions in use on our templates.
|
|
||||||
|
|
||||||
** HTML node content
|
|
||||||
|
|
||||||
There are two basic =lquery= functions that change the content of the assigned HTML node:
|
|
||||||
- =html= renders HTML elements as child
|
|
||||||
- =text= sets a text content to the element
|
|
||||||
|
|
||||||
|
|
||||||
The =lquery= s-expression accepts variable or lisp calls for any of the referred functions. For example, the following example sets the content of the =script= tag to the result of the =(asteroid::get-auth-state-js-var)= function:
|
|
||||||
|
|
||||||
#+begin_src html
|
|
||||||
<script lquery='(text (asteroid::get-auth-state-js-var))'></script>
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
*Note:* the template render process works on a different package, so call to application defined functions needs to include the package name.
|
|
||||||
|
|
||||||
** HTML node styling
|
|
||||||
|
|
||||||
=lquery= also grants easy styling customization of an HTML node. HTML styling can be accomplished in two ways:
|
|
||||||
- =css=, which sets the HTML =style= attribute of the element. It's called as a plist with all the properties to set.
|
|
||||||
- css classes that can be added (=add-class=) or removed (=remove-class=)
|
|
||||||
|
|
||||||
The following example adds the =display= and =marging= style properties to the document when the variable =framesetp= is true (=margin= is overwriten in this case):
|
|
||||||
|
|
||||||
#+begin_src html
|
|
||||||
<div style="margin: 15px 0;" lquery='(css :display (when framesetp "none") :margin (when framesetp "0"))'>
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
** HTML attributes
|
|
||||||
|
|
||||||
The =attr= function of the =lquery= s-expression allows customization of any valid HTML node attribute. The following example sets two attributes of an anchor tag:
|
|
||||||
- =href=, which uses the =eval= function to evaluate some lisp call
|
|
||||||
- =target= which only has a value when the variable =framesetp= is true
|
|
||||||
|
|
||||||
#+begin_src html
|
|
||||||
<a lquery='(attr :href (eval (format nil "/asteroid/~a" status-href)) :target (when framesetp "_self"))'>
|
|
||||||
#+end_src
|
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
(:listeners . ,total-listeners)
|
(:listeners . ,total-listeners)
|
||||||
(:track-id . ,(find-track-by-title title))))))))
|
(:track-id . ,(find-track-by-title title))))))))
|
||||||
|
|
||||||
(define-api-with-limit asteroid/partial/now-playing (&optional mount) (:limit 10 :timeout 1)
|
(define-api-with-limit asteroid/partial/now-playing (&optional mount) (:limit 180 :timeout 60)
|
||||||
"Get Partial HTML with live status from Icecast server.
|
"Get Partial HTML with live status from Icecast server.
|
||||||
Optional MOUNT parameter specifies which stream to get metadata from.
|
Optional MOUNT parameter specifies which stream to get metadata from.
|
||||||
Always polls both streams to keep recently played lists updated."
|
Always polls both streams to keep recently played lists updated."
|
||||||
|
|
@ -121,7 +121,7 @@
|
||||||
:connection-error t
|
:connection-error t
|
||||||
:stats nil))))))
|
:stats nil))))))
|
||||||
|
|
||||||
(define-api-with-limit asteroid/partial/now-playing-inline (&optional mount) (:limit 10 :timeout 1)
|
(define-api-with-limit asteroid/partial/now-playing-inline (&optional mount) (:limit 180 :timeout 60)
|
||||||
"Get inline text with now playing info (for admin dashboard and widgets).
|
"Get inline text with now playing info (for admin dashboard and widgets).
|
||||||
Optional MOUNT parameter specifies which stream to get metadata from."
|
Optional MOUNT parameter specifies which stream to get metadata from."
|
||||||
(with-error-handling
|
(with-error-handling
|
||||||
|
|
@ -135,7 +135,7 @@
|
||||||
(setf (header "Content-Type") "text/plain")
|
(setf (header "Content-Type") "text/plain")
|
||||||
"Stream Offline")))))
|
"Stream Offline")))))
|
||||||
|
|
||||||
(define-api-with-limit asteroid/partial/now-playing-json (&optional mount) (:limit 10 :timeout 1)
|
(define-api-with-limit asteroid/partial/now-playing-json (&optional mount) (:limit 180 :timeout 60)
|
||||||
"Get JSON with now playing info including track ID for favorites.
|
"Get JSON with now playing info including track ID for favorites.
|
||||||
Optional MOUNT parameter specifies which stream to get metadata from."
|
Optional MOUNT parameter specifies which stream to get metadata from."
|
||||||
;; Register web listener for geo stats (keeps listener active during playback)
|
;; Register web listener for geo stats (keeps listener active during playback)
|
||||||
|
|
|
||||||
17
limiter.lisp
17
limiter.lisp
|
|
@ -2,23 +2,6 @@
|
||||||
|
|
||||||
(in-package :asteroid)
|
(in-package :asteroid)
|
||||||
|
|
||||||
(defun cleanup-corrupted-rate-limits ()
|
|
||||||
"Clean up corrupted rate limit entries with negative amounts.
|
|
||||||
The r-simple-rate library has a bug where the reset condition only triggers
|
|
||||||
when amount >= 0, so negative amounts never reset. This function deletes
|
|
||||||
any corrupted entries so they can be recreated fresh."
|
|
||||||
(handler-case
|
|
||||||
(let ((deleted (db:remove 'simple-rate::tracking
|
|
||||||
(db:query (:< 'amount 0)))))
|
|
||||||
(when (and deleted (> deleted 0))
|
|
||||||
(l:info :rate-limiter "Cleaned up ~a corrupted rate limit entries" deleted)))
|
|
||||||
(error (e)
|
|
||||||
(l:warn :rate-limiter "Failed to cleanup rate limits: ~a" e))))
|
|
||||||
|
|
||||||
(define-trigger db:connected ()
|
|
||||||
"Clean up any corrupted rate limit entries on startup"
|
|
||||||
(cleanup-corrupted-rate-limits))
|
|
||||||
|
|
||||||
(defun render-rate-limit-error-page()
|
(defun render-rate-limit-error-page()
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(load-template "error")
|
(load-template "error")
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/asteroid/static/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/asteroid/static/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/asteroid/static/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/asteroid/static/favicon-16x16.png">
|
||||||
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||||
<script lquery='(text (asteroid::get-auth-state-js-var))'></script>
|
<script src="/asteroid/static/js/auth-ui.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
@ -19,9 +19,33 @@
|
||||||
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<c:h>(asteroid::load-template "partial/navbar")</c:h>
|
<nav class="nav">
|
||||||
|
<c:if test="(not framesetp)">
|
||||||
|
<c:then>
|
||||||
|
<a href="/asteroid/">Home</a>
|
||||||
|
<a href="/asteroid/player">Player</a>
|
||||||
|
<a href="/asteroid/about">About</a>
|
||||||
|
<a href="/asteroid/status">Status</a>
|
||||||
|
<a href="/asteroid/profile" data-show-if-logged-in>Profile</a>
|
||||||
|
<a href="/asteroid/admin" data-show-if-admin>Admin</a>
|
||||||
|
<a href="/asteroid/login" data-show-if-logged-out>Login</a>
|
||||||
|
<a href="/asteroid/register" data-show-if-logged-out>Register</a>
|
||||||
|
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
|
||||||
|
</c:then>
|
||||||
|
<c:else>
|
||||||
|
<a href="/asteroid/content" target="_self">Home</a>
|
||||||
|
<a href="/asteroid/player-content" target="_self">Player</a>
|
||||||
|
<a href="/asteroid/about-content" target="_self">About</a>
|
||||||
|
<a href="/asteroid/status-content" target="_self">Status</a>
|
||||||
|
<a href="/asteroid/profile" target="_self" data-show-if-logged-in>Profile</a>
|
||||||
|
<a href="/asteroid/admin" target="_self" data-show-if-admin>Admin</a>
|
||||||
|
<a href="/asteroid/login" target="_self" data-show-if-logged-out>Login</a>
|
||||||
|
<a href="/asteroid/register" target="_self" data-show-if-logged-out>Register</a>
|
||||||
|
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
|
||||||
|
</c:else>
|
||||||
|
</c:if>
|
||||||
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main style="max-width: 800px; margin: 0 auto; padding: 20px;">
|
<main style="max-width: 800px; margin: 0 auto; padding: 20px;">
|
||||||
<section style="margin-bottom: 30px;">
|
<section style="margin-bottom: 30px;">
|
||||||
<h2 style="color: #00ff00; border-bottom: 2px solid #00ff00; padding-bottom: 10px;">🎵 Asteroid Music for Hackers</h2>
|
<h2 style="color: #00ff00; border-bottom: 2px solid #00ff00; padding-bottom: 10px;">🎵 Asteroid Music for Hackers</h2>
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,19 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||||
<script lquery='(text (asteroid::get-auth-state-js-var))'></script>
|
<script src="/asteroid/static/js/auth-ui.js"></script>
|
||||||
<script src="/asteroid/static/js/admin.js"></script>
|
<script src="/asteroid/static/js/admin.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>🎛️ ADMIN DASHBOARD</h1>
|
<h1>🎛️ ADMIN DASHBOARD</h1>
|
||||||
<c:h>(asteroid::load-template "partial/navbar-admin")</c:h>
|
<div class="nav">
|
||||||
|
<a href="/asteroid">Home</a>
|
||||||
|
<a href="/asteroid/player">Player</a>
|
||||||
|
<a href="/asteroid/profile">Profile</a>
|
||||||
|
<a href="/asteroid/admin/users">👥 Users</a>
|
||||||
|
<a href="/asteroid/logout" class="btn-logout">Logout</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- System Status -->
|
<!-- System Status -->
|
||||||
<div class="admin-section">
|
<div class="admin-section">
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,11 @@
|
||||||
<span>ASTEROID RADIO</span>
|
<span>ASTEROID RADIO</span>
|
||||||
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
||||||
</h1>
|
</h1>
|
||||||
<c:h>(asteroid::load-template "partial/navbar")</c:h>
|
<nav class="nav">
|
||||||
|
<a href="/asteroid/">Home</a>
|
||||||
|
<a href="/asteroid/player">Player</a>
|
||||||
|
<a href="/asteroid/about">About</a>
|
||||||
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main style="max-width: 800px; margin: 0 auto; padding: 20px;">
|
<main style="max-width: 800px; margin: 0 auto; padding: 20px;">
|
||||||
<section style="margin-bottom: 30px;">
|
<section style="margin-bottom: 30px;">
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/asteroid/static/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/asteroid/static/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/asteroid/static/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/asteroid/static/favicon-16x16.png">
|
||||||
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||||
<script lquery='(text (asteroid::get-auth-state-js-var))'></script>
|
<script src="/asteroid/static/js/auth-ui.js"></script>
|
||||||
<script src="/asteroid/static/js/front-page.js"></script>
|
<script src="/asteroid/static/js/front-page.js"></script>
|
||||||
<script src="/asteroid/static/js/recently-played.js"></script>
|
<script src="/asteroid/static/js/recently-played.js"></script>
|
||||||
<script src="/api/asteroid/spectrum-analyzer.js"></script>
|
<script src="/api/asteroid/spectrum-analyzer.js"></script>
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
<h3 class="page-subtitle">The Station at the End of Time</h3>
|
<h3 class="page-subtitle">The Station at the End of Time</h3>
|
||||||
|
|
||||||
<!-- Spectrum Analyzer Canvas -->
|
<!-- Spectrum Analyzer Canvas -->
|
||||||
<div style="text-align: center; margin: 15px 0;" lquery='(css :display (when framesetp "none") :margin (when framesetp "0"))'>
|
<div style="text-align: center; margin: 15px 0;">
|
||||||
<canvas id="spectrum-canvas" width="800" height="100" style="max-width: 100%; border: 1px solid #00ff00; background: #000; border-radius: 4px;"></canvas>
|
<canvas id="spectrum-canvas" width="800" height="100" style="max-width: 100%; border: 1px solid #00ff00; background: #000; border-radius: 4px;"></canvas>
|
||||||
<div style="margin-top: 8px; font-size: 0.9em;">
|
<div style="margin-top: 8px; font-size: 0.9em;">
|
||||||
<label style="margin-right: 10px;">
|
<label style="margin-right: 10px;">
|
||||||
|
|
@ -50,7 +50,32 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<c:h>(asteroid::load-template "partial/navbar")</c:h>
|
<nav class="nav">
|
||||||
|
<c:if test="(not framesetp)">
|
||||||
|
<c:then>
|
||||||
|
<a href="/asteroid/">Home</a>
|
||||||
|
<a href="/asteroid/player">Player</a>
|
||||||
|
<a href="/asteroid/about">About</a>
|
||||||
|
<a href="/asteroid/status">Status</a>
|
||||||
|
<a href="/asteroid/profile" data-show-if-logged-in>Profile</a>
|
||||||
|
<a href="/asteroid/admin" data-show-if-admin>Admin</a>
|
||||||
|
<a href="/asteroid/login" data-show-if-logged-out>Login</a>
|
||||||
|
<a href="/asteroid/register" data-show-if-logged-out>Register</a>
|
||||||
|
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
|
||||||
|
</c:then>
|
||||||
|
<c:else>
|
||||||
|
<a href="/asteroid/content" target="_self">Home</a>
|
||||||
|
<a href="/asteroid/player-content" target="_self">Player</a>
|
||||||
|
<a href="/asteroid/about-content" target="_self">About</a>
|
||||||
|
<a href="/asteroid/status-content" target="_self">Status</a>
|
||||||
|
<a href="/asteroid/profile" target="_self" data-show-if-logged-in>Profile</a>
|
||||||
|
<a href="/asteroid/admin" target="_self" data-show-if-admin>Admin</a>
|
||||||
|
<a href="/asteroid/login" target="_self" data-show-if-logged-out>Login</a>
|
||||||
|
<a href="/asteroid/register" target="_self" data-show-if-logged-out>Register</a>
|
||||||
|
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
|
||||||
|
</c:else>
|
||||||
|
</c:if>
|
||||||
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,12 @@
|
||||||
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
||||||
<span>ASTEROID RADIO - LOGIN</span>
|
<span>ASTEROID RADIO - LOGIN</span>
|
||||||
</h1>
|
</h1>
|
||||||
<c:h>(asteroid::load-template "partial/navbar")</c:h>
|
<nav class="nav">
|
||||||
|
<a href="/asteroid">Home</a>
|
||||||
|
<a href="/asteroid/player">Player</a>
|
||||||
|
<a href="/asteroid/status">Status</a>
|
||||||
|
<a href="/asteroid/register">Register</a>
|
||||||
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="auth-container">
|
<div class="auth-container">
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
<c:let home-href='(if framesetp "content" "")'
|
|
||||||
player-href='(if framesetp "player-content" "player")'
|
|
||||||
current-user='(asteroid::get-current-user)'
|
|
||||||
framesetp='framesetp'>
|
|
||||||
<!-- Navbar definition -->
|
|
||||||
<nav class="nav">
|
|
||||||
<c:unless test='(asteroid::member-string "home" (** :navbar-exclude))'>
|
|
||||||
<a lquery='(attr :href (eval (format nil "/asteroid/~a" home-href)) :target (when framesetp "_self"))'>
|
|
||||||
Home
|
|
||||||
</a>
|
|
||||||
</c:unless>
|
|
||||||
<c:unless test='(asteroid::member-string "player" (** :navbar-exclude))'>
|
|
||||||
<a lquery='(attr :href (eval (format nil "/asteroid/~a" player-href)) :target (when framesetp "_self"))'>
|
|
||||||
Player
|
|
||||||
</a>
|
|
||||||
</c:unless>
|
|
||||||
<c:unless test='(asteroid::member-string "profile" (** :navbar-exclude))'>
|
|
||||||
<a href="/asteroid/profile"
|
|
||||||
lquery='(attr :target (when framesetp "_self"))'>
|
|
||||||
Profile
|
|
||||||
</a>
|
|
||||||
</c:unless>
|
|
||||||
<c:unless test='(asteroid::member-string "admin" (** :navbar-exclude))'>
|
|
||||||
<a href="/asteroid/admin"
|
|
||||||
lquery='(attr :target (when framesetp "_self"))'>
|
|
||||||
Admin
|
|
||||||
</a>
|
|
||||||
</c:unless>
|
|
||||||
<c:unless test='(asteroid::member-string "users" (** :navbar-exclude))'>
|
|
||||||
<a href="/asteroid/admin/users"
|
|
||||||
lquery='(attr :target (when framesetp "_self"))'>
|
|
||||||
👥 Users
|
|
||||||
</a>
|
|
||||||
</c:unless>
|
|
||||||
<a href="/asteroid/logout"
|
|
||||||
lquery='(attr :target (when framesetp "_self"))'
|
|
||||||
class="btn-logout">
|
|
||||||
Logout
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</c:let>
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
<c:let home-href='(if framesetp "content" "")'
|
|
||||||
player-href='(if framesetp "player-content" "player")'
|
|
||||||
about-href='(if framesetp "about-content" "about")'
|
|
||||||
status-href='(if framesetp "status-content" "status")'
|
|
||||||
current-user='(asteroid::get-current-user)'
|
|
||||||
framesetp='framesetp'>
|
|
||||||
<!-- Navbar definition -->
|
|
||||||
<nav class="nav">
|
|
||||||
<c:unless test='(asteroid::member-string "home" (** :navbar-exclude))'>
|
|
||||||
<a lquery='(attr :href (eval (format nil "/asteroid/~a" home-href)) :target (when framesetp "_self"))'>
|
|
||||||
Home
|
|
||||||
</a>
|
|
||||||
</c:unless>
|
|
||||||
<c:unless test='(asteroid::member-string "player" (** :navbar-exclude))'>
|
|
||||||
<a lquery='(attr :href (eval (format nil "/asteroid/~a" player-href)) :target (when framesetp "_self"))'>
|
|
||||||
Player
|
|
||||||
</a>
|
|
||||||
</c:unless>
|
|
||||||
<c:unless test='(asteroid::member-string "about" (** :navbar-exclude))'>
|
|
||||||
<a lquery='(attr :href (eval (format nil "/asteroid/~a" about-href)) :target (when framesetp "_self"))'>
|
|
||||||
About
|
|
||||||
</a>
|
|
||||||
</c:unless>
|
|
||||||
<c:unless test='(asteroid::member-string "status" (** :navbar-exclude))'>
|
|
||||||
<a lquery='(attr :href (eval (format nil "/asteroid/~a" status-href)) :target (when framesetp "_self"))'>
|
|
||||||
Status
|
|
||||||
</a>
|
|
||||||
</c:unless>
|
|
||||||
<c:when test='(and current-user (not (asteroid::member-string "profile" (** :navbar-exclude))))'>
|
|
||||||
<a href="/asteroid/profile"
|
|
||||||
lquery='(attr :target (when framesetp "_self"))'>
|
|
||||||
Profile
|
|
||||||
</a>
|
|
||||||
</c:when>
|
|
||||||
<c:when test='(and (equal "admin" (clip current-user :role)) (not (asteroid::member-string "admin" (** :navbar-exclude))))'>
|
|
||||||
<a href="/asteroid/admin"
|
|
||||||
lquery='(attr :target (when framesetp "_self"))'>
|
|
||||||
Admin
|
|
||||||
</a>
|
|
||||||
</c:when>
|
|
||||||
<c:when test="(not current-user)">
|
|
||||||
<a href="/asteroid/login"
|
|
||||||
lquery='(attr :target (when framesetp "_self"))'>
|
|
||||||
Login
|
|
||||||
</a>
|
|
||||||
</c:when>
|
|
||||||
<c:when test="(not current-user)">
|
|
||||||
<a href="/asteroid/register"
|
|
||||||
lquery='(attr :target (when framesetp "_self"))'>
|
|
||||||
Register
|
|
||||||
</a>
|
|
||||||
</c:when>
|
|
||||||
<c:when test="current-user">
|
|
||||||
<a href="/asteroid/logout"
|
|
||||||
lquery='(attr :target (when framesetp "_self"))'
|
|
||||||
class="btn-logout">
|
|
||||||
Logout
|
|
||||||
</a>
|
|
||||||
</c:when>
|
|
||||||
</nav>
|
|
||||||
</c:let>
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/asteroid/static/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/asteroid/static/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/asteroid/static/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/asteroid/static/favicon-16x16.png">
|
||||||
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||||
<script lquery='(text (asteroid::get-auth-state-js-var))'></script>
|
<script src="/asteroid/static/js/auth-ui.js"></script>
|
||||||
<script src="/asteroid/static/js/front-page.js"></script>
|
<script src="/asteroid/static/js/front-page.js"></script>
|
||||||
<script src="/asteroid/static/js/player.js"></script>
|
<script src="/asteroid/static/js/player.js"></script>
|
||||||
<c:if test="framesetp">
|
<c:if test="framesetp">
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
<!-- Spectrum Analyzer Canvas -->
|
<!-- Spectrum Analyzer Canvas -->
|
||||||
<c:if test="framesetp">
|
<c:if test="framesetp">
|
||||||
<c:then>
|
<c:then>
|
||||||
<div style="text-align: center; margin: 15px 0; display: none;">
|
<div style="text-align: center; margin: 15px 0;">
|
||||||
<canvas id="spectrum-canvas" width="800" height="100" style="max-width: 100%; border: 1px solid #00ff00; background: #000; border-radius: 4px;"></canvas>
|
<canvas id="spectrum-canvas" width="800" height="100" style="max-width: 100%; border: 1px solid #00ff00; background: #000; border-radius: 4px;"></canvas>
|
||||||
<div style="margin-top: 8px; font-size: 0.9em;">
|
<div style="margin-top: 8px; font-size: 0.9em;">
|
||||||
<label style="margin-right: 10px;">
|
<label style="margin-right: 10px;">
|
||||||
|
|
@ -56,7 +56,26 @@
|
||||||
</c:then>
|
</c:then>
|
||||||
</c:if>
|
</c:if>
|
||||||
|
|
||||||
<c:h>(asteroid::load-template "partial/navbar")</c:h>
|
<div class="nav">
|
||||||
|
<c:if test="(not framesetp)">
|
||||||
|
<c:then>
|
||||||
|
<a href="/asteroid">Home</a>
|
||||||
|
<a href="/asteroid/profile">Profile</a>
|
||||||
|
<a href="/asteroid/admin" data-show-if-admin>Admin</a>
|
||||||
|
<a href="/asteroid/login" data-show-if-logged-out>Login</a>
|
||||||
|
<a href="/asteroid/register" data-show-if-logged-out>Register</a>
|
||||||
|
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
|
||||||
|
</c:then>
|
||||||
|
<c:else>
|
||||||
|
<a href="/asteroid/content" target="content-frame">Home</a>
|
||||||
|
<a href="/asteroid/profile" target="content-frame" data-show-if-logged-in>Profile</a>
|
||||||
|
<a href="/asteroid/admin" target="content-frame" data-show-if-admin>Admin</a>
|
||||||
|
<a href="/asteroid/login" target="content-frame" data-show-if-logged-out>Login</a>
|
||||||
|
<a href="/asteroid/register" target="content-frame" data-show-if-logged-out>Register</a>
|
||||||
|
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
|
||||||
|
</c:else>
|
||||||
|
</c:if>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Live Stream Section -->
|
<!-- Live Stream Section -->
|
||||||
<div class="player-section">
|
<div class="player-section">
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,18 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||||
<script lquery='(text (asteroid::get-auth-state-js-var))'></script>
|
<script src="/asteroid/static/js/auth-ui.js"></script>
|
||||||
<script src="/asteroid/static/js/profile.js"></script>
|
<script src="/asteroid/static/js/profile.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>👤 USER PROFILE</h1>
|
<h1>👤 USER PROFILE</h1>
|
||||||
<c:h>(asteroid::load-template "partial/navbar")</c:h>
|
<div class="nav">
|
||||||
|
<a href="/asteroid">Home</a>
|
||||||
|
<a href="/asteroid/player">Player</a>
|
||||||
|
<a href="/asteroid/admin" data-show-if-admin>Admin</a>
|
||||||
|
<a href="/asteroid/logout" class="btn-logout">Logout</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- User Profile Header -->
|
<!-- User Profile Header -->
|
||||||
<div class="admin-section">
|
<div class="admin-section">
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,12 @@
|
||||||
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
||||||
<span>ASTEROID RADIO - REGISTER</span>
|
<span>ASTEROID RADIO - REGISTER</span>
|
||||||
</h1>
|
</h1>
|
||||||
<c:h>(asteroid::load-template "partial/navbar")</c:h>
|
<nav class="nav">
|
||||||
|
<a href="/asteroid">Home</a>
|
||||||
|
<a href="/asteroid/player">Player</a>
|
||||||
|
<a href="/asteroid/status">Status</a>
|
||||||
|
<a href="/asteroid/login">Login</a>
|
||||||
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="auth-container">
|
<div class="auth-container">
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||||
<script lquery='(text (asteroid::get-auth-state-js-var))'></script>
|
<script src="/asteroid/static/js/auth-ui.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
@ -16,7 +16,33 @@
|
||||||
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<c:h>(asteroid::load-template "partial/navbar")</c:h>
|
<nav class="nav">
|
||||||
|
<c:if test="(not framesetp)">
|
||||||
|
<c:then>
|
||||||
|
<a href="/asteroid">Home</a>
|
||||||
|
<a href="/asteroid/player">Player</a>
|
||||||
|
<a href="/asteroid/about">About</a>
|
||||||
|
<a href="/asteroid/status">Status</a>
|
||||||
|
<a href="/asteroid/profile" data-show-if-logged-in>Profile</a>
|
||||||
|
<a href="/asteroid/admin" data-show-if-admin>Admin</a>
|
||||||
|
<a href="/asteroid/login" data-show-if-logged-out>Login</a>
|
||||||
|
<a href="/asteroid/register" data-show-if-logged-out>Register</a>
|
||||||
|
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
|
||||||
|
</c:then>
|
||||||
|
<c:else>
|
||||||
|
<a href="/asteroid/content" target="_self">Home</a>
|
||||||
|
<a href="/asteroid/player-content" target="_self">Player</a>
|
||||||
|
<a href="/asteroid/about-content" target="_self">About</a>
|
||||||
|
<a href="/asteroid/status-content" target="_self">Status</a>
|
||||||
|
<a href="/asteroid/profile" target="_self" data-show-if-logged-in>Profile</a>
|
||||||
|
<a href="/asteroid/admin" target="_self" data-show-if-admin>Admin</a>
|
||||||
|
<a href="/asteroid/login" target="_self" data-show-if-logged-out>Login</a>
|
||||||
|
<a href="/asteroid/register" target="_self" data-show-if-logged-out>Register</a>
|
||||||
|
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
|
||||||
|
</c:else>
|
||||||
|
</c:if>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
<main style="max-width: 800px; margin: 0 auto; padding: 20px;">
|
<main style="max-width: 800px; margin: 0 auto; padding: 20px;">
|
||||||
<section style="margin-bottom: 30px;">
|
<section style="margin-bottom: 30px;">
|
||||||
|
|
@ -35,14 +61,12 @@
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<c:when test='(equal "admin" (clip (asteroid::get-current-user) :role))'>
|
|
||||||
<section style="margin-bottom: 30px;">
|
<section style="margin-bottom: 30px;">
|
||||||
<h2 style="color: #00ff00; border-bottom: 2px solid #00ff00; padding-bottom: 10px;">ℹ️ Additional Information</h2>
|
<h2 style="color: #00ff00; border-bottom: 2px solid #00ff00; padding-bottom: 10px;">ℹ️ Additional Information</h2>
|
||||||
<p style="line-height: 1.6;">
|
<p style="line-height: 1.6;">
|
||||||
For detailed system status and administration, please visit the <a href="/asteroid/admin" style="color: #00ff00;">Admin Dashboard</a>.
|
For detailed system status and administration, please visit the <a href="/asteroid/admin" style="color: #00ff00;" data-show-if-admin>Admin Dashboard</a>.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</c:when>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,11 @@
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>👥 USER MANAGEMENT</h1>
|
<h1>👥 USER MANAGEMENT</h1>
|
||||||
<c:h>(asteroid::load-template "partial/navbar-admin")</c:h>
|
<div class="nav">
|
||||||
|
<a href="/asteroid">Home</a>
|
||||||
|
<a href="/asteroid/admin">Admin</a>
|
||||||
|
<a href="/asteroid/logout" class="btn-logout">Logout</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- User Statistics -->
|
<!-- User Statistics -->
|
||||||
<div class="admin-section">
|
<div class="admin-section">
|
||||||
|
|
|
||||||
|
|
@ -157,17 +157,6 @@
|
||||||
"Get the currently authenticated user's ID from session"
|
"Get the currently authenticated user's ID from session"
|
||||||
(session:field "user-id"))
|
(session:field "user-id"))
|
||||||
|
|
||||||
(defun get-auth-state-js-var ()
|
|
||||||
"Builds a JavaScript variable definition with the current authentication state
|
|
||||||
for a request. The variable definition is a string ready to be injected in a
|
|
||||||
template file."
|
|
||||||
(let ((user (get-current-user)))
|
|
||||||
(format nil "var AUTHSTATE = ~a"
|
|
||||||
(cl-json:encode-json-to-string
|
|
||||||
`(("loggedIn" . ,(when user t))
|
|
||||||
("isAdmin" . ,(when (and user (user-has-role-p user :admin)) t))
|
|
||||||
("username" . ,(when user (dm:field user "username"))))))))
|
|
||||||
|
|
||||||
(defun require-authentication (&key (api nil))
|
(defun require-authentication (&key (api nil))
|
||||||
"Require user to be authenticated.
|
"Require user to be authenticated.
|
||||||
Returns T if authenticated, NIL if not (after emitting error response).
|
Returns T if authenticated, NIL if not (after emitting error response).
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue