DemoPage.vala 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. using Astralis;
  2. using Invercargill;
  3. using Invercargill.DataStructures;
  4. using Inversion;
  5. using Spry;
  6. /**
  7. * DemoPage - Interactive demo page
  8. *
  9. * Features a live Aurora Borealis simulation and counter that use
  10. * Server-Sent Events (SSE) for real-time updates.
  11. *
  12. * Updates are pushed when users click action buttons - no polling!
  13. */
  14. public class DemoPage : PageComponent {
  15. public const string ROUTE = "/demo";
  16. private AuroraState aurora_state = inject<AuroraState>();
  17. private ComponentFactory factory = inject<ComponentFactory>();
  18. public override string markup { get {
  19. return """
  20. <div sid="demo-page" class="section" hx-ext="sse" sse-connect="/sse/aurora" sse-close="close">
  21. <div class="container">
  22. <!-- Header -->
  23. <div class="section-header">
  24. <h1>Interactive Demo</h1>
  25. <p>Experience the power of Spry with real-time Server-Sent Events</p>
  26. </div>
  27. <!-- Aurora Demo -->
  28. <section class="demo-section">
  29. <div class="demo-header">
  30. <h2>🌌 Live Aurora Borealis</h2>
  31. <p style="color: var(--color-text-muted); margin-bottom: var(--space-4);">
  32. This aurora uses SSE for real-time updates! Click a button to update all connected clients.
  33. </p>
  34. <div class="demo-controls">
  35. <button class="demo-btn" hx-post="/aurora/action/boost" hx-swap="none">
  36. 🌞 Boost Solar Wind
  37. </button>
  38. <button class="demo-btn" hx-post="/aurora/action/calm" hx-swap="none">
  39. 🌙 Calm Aurora
  40. </button>
  41. <button class="demo-btn" hx-post="/aurora/action/shift" hx-swap="none">
  42. 🎨 Shift Colors
  43. </button>
  44. <button class="demo-btn" hx-post="/aurora/action/wave" hx-swap="none">
  45. 🌊 Add Wave
  46. </button>
  47. </div>
  48. </div>
  49. <!-- Aurora Canvas - updated via SSE -->
  50. <div class="aurora-canvas" sse-swap="aurora-canvas" hx-swap="innerHTML transition:true">
  51. <spry-outlet sid="aurora-waves"/>
  52. </div>
  53. <!-- Aurora Stats - updated via SSE -->
  54. <div class="aurora-stats" sse-swap="aurora-stats" hx-swap="innerHTML transition:true">
  55. <div class="aurora-stat">
  56. <div class="aurora-stat-value" sid="solar-wind"></div>
  57. <div class="aurora-stat-label">Solar Wind</div>
  58. </div>
  59. <div class="aurora-stat">
  60. <div class="aurora-stat-value" sid="wave-count"></div>
  61. <div class="aurora-stat-label">Waves</div>
  62. </div>
  63. <div class="aurora-stat">
  64. <div class="aurora-stat-value" sid="intensity"></div>
  65. <div class="aurora-stat-label">Intensity</div>
  66. </div>
  67. <div class="aurora-stat">
  68. <div class="aurora-stat-value" sid="color-mode"></div>
  69. <div class="aurora-stat-label">Color Mode</div>
  70. </div>
  71. </div>
  72. <p style="margin-top: var(--space-4); text-align: center; color: var(--color-text-muted);">
  73. Updates are pushed via SSE when anyone clicks the buttons. Try it from multiple browsers!
  74. </p>
  75. </section>
  76. <!-- Counter Demo -->
  77. <section class="demo-section" hx-ext="sse" sse-connect="/sse/counter" sse-close="close">
  78. <h2>🔢 Shared Counter</h2>
  79. <p>A counter that syncs across all connected clients using SSE.</p>
  80. <div style="display: flex; align-items: center; gap: var(--space-4); justify-content: center; margin: var(--space-6) 0;">
  81. <div class="stat-card" style="text-align: center; min-width: 200px;">
  82. <div class="stat-value" sse-swap="counter-value" hx-swap="innerHTML"></div>
  83. <div class="stat-label">Current Count</div>
  84. </div>
  85. </div>
  86. <div style="display: flex; gap: var(--space-3); justify-content: center;">
  87. <button class="demo-btn" hx-post="/counter/action/decrement" hx-swap="none">
  88. − Decrement
  89. </button>
  90. <button class="demo-btn" hx-post="/counter/action/reset" hx-swap="none">
  91. ↺ Reset
  92. </button>
  93. <button class="demo-btn" hx-post="/counter/action/increment" hx-swap="none">
  94. + Increment
  95. </button>
  96. </div>
  97. </section>
  98. <!-- How It Works -->
  99. <section class="demo-section">
  100. <h2>⚙ How It Works</h2>
  101. <p>This demo showcases Server-Sent Events (SSE) for real-time updates:</p>
  102. <div class="features-grid" style="margin-top: var(--space-6);">
  103. <div class="feature-card">
  104. <div class="feature-icon purple">📡</div>
  105. <h3>Server-Sent Events</h3>
  106. <p>SSE pushes updates to all connected clients instantly when actions occur.</p>
  107. </div>
  108. <div class="feature-card">
  109. <div class="feature-icon blue">🔗</div>
  110. <h3>Shared State</h3>
  111. <p>AuroraState is a singleton - all visitors see the same aurora and counter!</p>
  112. </div>
  113. <div class="feature-card">
  114. <div class="feature-icon green">⚡</div>
  115. <h3>Event-Driven</h3>
  116. <p>No polling! Updates only happen when someone clicks a button.</p>
  117. </div>
  118. </div>
  119. <div class="code-block" style="margin-top: var(--space-6);">
  120. <div class="code-header">
  121. <div class="code-dots">
  122. <div class="code-dot red"></div>
  123. <div class="code-dot yellow"></div>
  124. <div class="code-dot green"></div>
  125. </div>
  126. <span class="code-lang">HTML</span>
  127. </div>
  128. <pre><code>&lt;!-- SSE connection on parent --&gt;
  129. &lt;div hx-ext="sse" sse-connect="/sse/aurora"&gt;
  130. &lt;!-- Auto-updates when "aurora-canvas" event received --&gt;
  131. &lt;div sse-swap="aurora-canvas"&gt;
  132. Aurora waves render here
  133. &lt;/div&gt;
  134. &lt;/div&gt;
  135. &lt;!-- Button triggers action endpoint --&gt;
  136. &lt;button hx-post="/aurora/action/boost" hx-swap="none"&gt;
  137. Boost Solar Wind
  138. &lt;/button&gt;</code></pre>
  139. </div>
  140. </section>
  141. <!-- CTA -->
  142. <div class="text-center" style="margin-top: var(--space-12);">
  143. <h2>Want to Learn More?</h2>
  144. <p style="margin-bottom: var(--space-6);">Explore the features that make Spry powerful.</p>
  145. <div class="hero-actions" style="justify-content: center;">
  146. <a href="/features" class="btn btn-primary">View Features</a>
  147. <a href="/ecosystem" class="btn btn-secondary">See Ecosystem</a>
  148. </div>
  149. </div>
  150. </div>
  151. </div>
  152. """;
  153. }}
  154. public override async void prepare() throws Error {
  155. // Render initial aurora waves
  156. var waves = new Series<Renderable>();
  157. int wave_num = 0;
  158. foreach (var wave in aurora_state.get_waves()) {
  159. var component = factory.create<AuroraWaveComponent>();
  160. component.wave_id = wave_num;
  161. component.y_offset = wave.y_offset;
  162. component.amplitude = wave.amplitude;
  163. component.frequency = wave.frequency;
  164. component.color1 = wave.color1;
  165. component.color2 = wave.color2;
  166. component.opacity = wave.opacity;
  167. component.animation_delay = wave.animation_delay;
  168. waves.add(component);
  169. wave_num++;
  170. }
  171. set_outlet_children("aurora-waves", waves);
  172. // Update stats
  173. this["solar-wind"].text_content = "%.1f km/s".printf(aurora_state.solar_wind_speed);
  174. this["wave-count"].text_content = aurora_state.wave_count.to_string();
  175. this["intensity"].text_content = "%.0f%%".printf(aurora_state.intensity * 100);
  176. this["color-mode"].text_content = aurora_state.color_mode;
  177. // Update counter (initial value - will be updated via SSE)
  178. // Note: We need to set the stat-value div's content
  179. this["counter-value"].text_content = aurora_state.counter.to_string();
  180. }
  181. }
  182. /**
  183. * AuroraWaveData - Data for a single aurora wave
  184. */
  185. public class AuroraWaveData : Object {
  186. public double y_offset { get; set; }
  187. public double amplitude { get; set; }
  188. public double frequency { get; set; }
  189. public string color1 { get; set; default = "#7c3aed"; }
  190. public string color2 { get; set; default = "#22c55e"; }
  191. public double opacity { get; set; default = 0.6; }
  192. public double animation_delay { get; set; default = 0; }
  193. }
  194. /**
  195. * ColorPalette - A pair of colors for aurora waves
  196. */
  197. public class ColorPalette : Object {
  198. public string color1 { get; construct set; }
  199. public string color2 { get; construct set; }
  200. public string name { get; construct set; }
  201. public ColorPalette(string color1, string color2, string name) {
  202. Object(color1: color1, color2: color2, name: name);
  203. }
  204. }
  205. /**
  206. * AuroraState - Singleton state for the aurora simulation
  207. */
  208. public class AuroraState : Object {
  209. private GenericArray<AuroraWaveData> waves = new GenericArray<AuroraWaveData>();
  210. private Series<ColorPalette> color_palettes;
  211. public double solar_wind_speed { get; set; default = 400.0; }
  212. public double intensity { get; set; default = 0.5; }
  213. public string color_mode { get; set; default = "Green"; }
  214. public int counter { get; set; default = 0; }
  215. private int current_palette = 0;
  216. public int wave_count {
  217. get { return (int)waves.length; }
  218. }
  219. construct {
  220. // Initialize color palettes using Series
  221. color_palettes = new Series<ColorPalette>();
  222. color_palettes.add(new ColorPalette("#22c55e", "#16a34a", "Green"));
  223. color_palettes.add(new ColorPalette("#7c3aed", "#a855f7", "Purple"));
  224. color_palettes.add(new ColorPalette("#2563eb", "#3b82f6", "Blue"));
  225. color_palettes.add(new ColorPalette("#22c55e", "#7c3aed", "Mixed"));
  226. color_palettes.add(new ColorPalette("#f59e0b", "#ef4444", "Fire"));
  227. color_palettes.add(new ColorPalette("#06b6d4", "#22c55e", "Ocean"));
  228. // Start with some waves
  229. add_wave();
  230. add_wave();
  231. add_wave();
  232. }
  233. private ColorPalette get_current_palette() {
  234. uint idx = (uint)current_palette;
  235. return color_palettes.element_at_or_default(idx);
  236. }
  237. public void add_wave() {
  238. var palette = get_current_palette();
  239. var wave = new AuroraWaveData() {
  240. y_offset = 20 + Random.double_range(0, 40),
  241. amplitude = 10 + Random.double_range(0, 20),
  242. frequency = 0.5 + Random.double_range(0, 1.5),
  243. color1 = palette.color1,
  244. color2 = palette.color2,
  245. opacity = 0.3 + Random.double_range(0, 0.5),
  246. animation_delay = Random.double_range(0, 3)
  247. };
  248. waves.add(wave);
  249. // Keep max 8 waves
  250. if (waves.length > 8) {
  251. waves.remove_index(0);
  252. }
  253. }
  254. public void boost_solar_wind() {
  255. solar_wind_speed = 600 + Random.double_range(0, 400);
  256. intensity = 0.8 + Random.double_range(0, 0.2);
  257. // Increase all wave amplitudes
  258. for (int i = 0; i < waves.length; i++) {
  259. var w = waves.get(i);
  260. w.amplitude *= 1.3;
  261. w.opacity = double.min(0.9, w.opacity + 0.1);
  262. }
  263. }
  264. public void calm() {
  265. solar_wind_speed = 300 + Random.double_range(0, 100);
  266. intensity = 0.3 + Random.double_range(0, 0.2);
  267. // Decrease all wave amplitudes
  268. for (int i = 0; i < waves.length; i++) {
  269. var w = waves.get(i);
  270. w.amplitude *= 0.7;
  271. w.opacity = double.max(0.2, w.opacity - 0.1);
  272. }
  273. }
  274. public void shift_colors() {
  275. current_palette = (current_palette + 1) % 6;
  276. var palette = get_current_palette();
  277. // Update color mode name
  278. color_mode = palette.name;
  279. // Update all wave colors
  280. for (int i = 0; i < waves.length; i++) {
  281. var w = waves.get(i);
  282. w.color1 = palette.color1;
  283. w.color2 = palette.color2;
  284. }
  285. }
  286. public void evolve() {
  287. // Called periodically to naturally evolve the aurora
  288. solar_wind_speed = 300 + Random.double_range(0, 300);
  289. intensity = 0.4 + Random.double_range(0, 0.4);
  290. // Slightly modify waves
  291. for (int i = 0; i < waves.length; i++) {
  292. var w = waves.get(i);
  293. w.y_offset += Random.double_range(-5, 5);
  294. w.y_offset = double.max(10, double.min(70, w.y_offset));
  295. w.amplitude += Random.double_range(-3, 3);
  296. w.amplitude = double.max(5, double.min(40, w.amplitude));
  297. }
  298. }
  299. public GenericArray<AuroraWaveData> get_waves() {
  300. return waves;
  301. }
  302. }