Compare commits
No commits in common. "d894964c204a4b2762f82ff6e07f03d771d12a40" and "a9c48e59c951b5e5251d1b61fdcd2e33c597a174" have entirely different histories.
d894964c20
...
a9c48e59c9
|
|
@ -4,8 +4,7 @@
|
|||
(:use :cl)
|
||||
(:export :internal-disable-debugger)
|
||||
(:export :internal-quit
|
||||
:pht
|
||||
:member-string))
|
||||
:pht))
|
||||
|
||||
(in-package :asteroid.app-utils)
|
||||
|
||||
|
|
@ -20,11 +19,6 @@
|
|||
(internal-quit)))
|
||||
(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)
|
||||
"Taken from the cliki"
|
||||
;; This group from "clocc-port/ext.lisp"
|
||||
|
|
|
|||
|
|
@ -1009,7 +1009,6 @@
|
|||
(error () 0))))
|
||||
(clip:process-to-string
|
||||
(load-template "admin")
|
||||
:navbar-exclude '("admin")
|
||||
:title "🎵 ASTEROID RADIO - Admin Dashboard"
|
||||
:server-status "🟢 Running"
|
||||
:database-status (handler-case
|
||||
|
|
@ -1028,7 +1027,6 @@
|
|||
(require-authentication)
|
||||
(clip:process-to-string
|
||||
(load-template "users")
|
||||
:navbar-exclude '("profile" "users")
|
||||
:title "ASTEROID RADIO - User Management"))
|
||||
|
||||
;; User Profile page (requires authentication)
|
||||
|
|
@ -1037,7 +1035,6 @@
|
|||
(require-authentication)
|
||||
(clip:process-to-string
|
||||
(load-template "profile")
|
||||
:navbar-exclude '("about" "status" "profile")
|
||||
:title "🎧 admin - Profile | Asteroid Radio"
|
||||
:username "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)
|
||||
(: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.
|
||||
Optional MOUNT parameter specifies which stream to get metadata from.
|
||||
Always polls both streams to keep recently played lists updated."
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
:connection-error t
|
||||
: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).
|
||||
Optional MOUNT parameter specifies which stream to get metadata from."
|
||||
(with-error-handling
|
||||
|
|
@ -135,7 +135,7 @@
|
|||
(setf (header "Content-Type") "text/plain")
|
||||
"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.
|
||||
Optional MOUNT parameter specifies which stream to get metadata from."
|
||||
;; Register web listener for geo stats (keeps listener active during playback)
|
||||
|
|
|
|||
17
limiter.lisp
17
limiter.lisp
|
|
@ -2,23 +2,6 @@
|
|||
|
||||
(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()
|
||||
(clip:process-to-string
|
||||
(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="16x16" href="/asteroid/static/favicon-16x16.png">
|
||||
<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>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
|
@ -19,9 +19,33 @@
|
|||
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
||||
</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;">
|
||||
<section style="margin-bottom: 30px;">
|
||||
<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 name="viewport" content="width=device-width, initial-scale=1">
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<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 -->
|
||||
<div class="admin-section">
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@
|
|||
<span>ASTEROID RADIO</span>
|
||||
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
||||
</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>
|
||||
<main style="max-width: 800px; margin: 0 auto; padding: 20px;">
|
||||
<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="16x16" href="/asteroid/static/favicon-16x16.png">
|
||||
<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/recently-played.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>
|
||||
|
||||
<!-- 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>
|
||||
<div style="margin-top: 8px; font-size: 0.9em;">
|
||||
<label style="margin-right: 10px;">
|
||||
|
|
@ -50,7 +50,32 @@
|
|||
</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>
|
||||
|
||||
<main>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,12 @@
|
|||
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
||||
<span>ASTEROID RADIO - LOGIN</span>
|
||||
</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>
|
||||
|
||||
<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="16x16" href="/asteroid/static/favicon-16x16.png">
|
||||
<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/player.js"></script>
|
||||
<c:if test="framesetp">
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
<!-- Spectrum Analyzer Canvas -->
|
||||
<c:if test="framesetp">
|
||||
<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>
|
||||
<div style="margin-top: 8px; font-size: 0.9em;">
|
||||
<label style="margin-right: 10px;">
|
||||
|
|
@ -56,7 +56,26 @@
|
|||
</c:then>
|
||||
</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 -->
|
||||
<div class="player-section">
|
||||
|
|
|
|||
|
|
@ -5,13 +5,18 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<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 -->
|
||||
<div class="admin-section">
|
||||
|
|
|
|||
|
|
@ -16,7 +16,12 @@
|
|||
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
||||
<span>ASTEROID RADIO - REGISTER</span>
|
||||
</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>
|
||||
|
||||
<div class="auth-container">
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<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>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
|
@ -16,7 +16,33 @@
|
|||
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
||||
</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;">
|
||||
<section style="margin-bottom: 30px;">
|
||||
|
|
@ -35,14 +61,12 @@
|
|||
</ul>
|
||||
</section>
|
||||
|
||||
<c:when test='(equal "admin" (clip (asteroid::get-current-user) :role))'>
|
||||
<section style="margin-bottom: 30px;">
|
||||
<h2 style="color: #00ff00; border-bottom: 2px solid #00ff00; padding-bottom: 10px;">ℹ️ Additional Information</h2>
|
||||
<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>
|
||||
</section>
|
||||
</c:when>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@
|
|||
<body>
|
||||
<div class="container">
|
||||
<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 -->
|
||||
<div class="admin-section">
|
||||
|
|
|
|||
|
|
@ -157,17 +157,6 @@
|
|||
"Get the currently authenticated user's ID from session"
|
||||
(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))
|
||||
"Require user to be authenticated.
|
||||
Returns T if authenticated, NIL if not (after emitting error response).
|
||||
|
|
|
|||
Loading…
Reference in New Issue