|
@@ -57,16 +57,164 @@ class AppState : Object {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * CSS content for the counter page.
|
|
|
|
|
+ * Served as a FastResource for optimal performance with pre-compression.
|
|
|
|
|
+ */
|
|
|
|
|
+private const string COUNTER_CSS = """
|
|
|
|
|
+body {
|
|
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
|
|
+ max-width: 700px;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+}
|
|
|
|
|
+.card {
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ padding: 25px;
|
|
|
|
|
+ margin: 15px 0;
|
|
|
|
|
+ box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
|
|
|
|
+}
|
|
|
|
|
+h1 {
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ margin-top: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.counter-display {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 30px;
|
|
|
|
|
+ background: #f8f9fa;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ margin: 20px 0;
|
|
|
|
|
+}
|
|
|
|
|
+.counter-value {
|
|
|
|
|
+ font-size: 72px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ color: #667eea;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+}
|
|
|
|
|
+.counter-label {
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ text-transform: uppercase;
|
|
|
|
|
+ letter-spacing: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+.button-group {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ margin: 20px 0;
|
|
|
|
|
+}
|
|
|
|
|
+button {
|
|
|
|
|
+ padding: 12px 24px;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+}
|
|
|
|
|
+button:hover {
|
|
|
|
|
+ transform: translateY(-2px);
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
|
|
|
+}
|
|
|
|
|
+.btn-primary {
|
|
|
|
|
+ background: #667eea;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+}
|
|
|
|
|
+.btn-primary:hover {
|
|
|
|
|
+ background: #5a6fd6;
|
|
|
|
|
+}
|
|
|
|
|
+.btn-danger {
|
|
|
|
|
+ background: #e74c3c;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+}
|
|
|
|
|
+.btn-danger:hover {
|
|
|
|
|
+ background: #c0392b;
|
|
|
|
|
+}
|
|
|
|
|
+.btn-success {
|
|
|
|
|
+ background: #2ecc71;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+}
|
|
|
|
|
+.btn-success:hover {
|
|
|
|
|
+ background: #27ae60;
|
|
|
|
|
+}
|
|
|
|
|
+.info-grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
+ gap: 15px;
|
|
|
|
|
+ margin: 20px 0;
|
|
|
|
|
+}
|
|
|
|
|
+.info-item {
|
|
|
|
|
+ background: #f8f9fa;
|
|
|
|
|
+ padding: 15px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+}
|
|
|
|
|
+.info-label {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ text-transform: uppercase;
|
|
|
|
|
+ letter-spacing: 1px;
|
|
|
|
|
+}
|
|
|
|
|
+.info-value {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ margin-top: 5px;
|
|
|
|
|
+}
|
|
|
|
|
+.status-positive {
|
|
|
|
|
+ color: #2ecc71 !important;
|
|
|
|
|
+}
|
|
|
|
|
+.status-negative {
|
|
|
|
|
+ color: #e74c3c !important;
|
|
|
|
|
+}
|
|
|
|
|
+.status-zero {
|
|
|
|
|
+ color: #666 !important;
|
|
|
|
|
+}
|
|
|
|
|
+code {
|
|
|
|
|
+ background: #e8e8e8;
|
|
|
|
|
+ padding: 2px 6px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+pre {
|
|
|
|
|
+ background: #263238;
|
|
|
|
|
+ color: #aed581;
|
|
|
|
|
+ padding: 15px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ overflow-x: auto;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+}
|
|
|
|
|
+.feature-list {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ padding-left: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+.feature-list li {
|
|
|
|
|
+ margin: 8px 0;
|
|
|
|
|
+ color: #555;
|
|
|
|
|
+}
|
|
|
|
|
+a {
|
|
|
|
|
+ color: #667eea;
|
|
|
|
|
+ text-decoration: none;
|
|
|
|
|
+}
|
|
|
|
|
+a:hover {
|
|
|
|
|
+ text-decoration: underline;
|
|
|
|
|
+}
|
|
|
|
|
+""";
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* CounterTemplate - A cached HTML template for the counter page.
|
|
* CounterTemplate - A cached HTML template for the counter page.
|
|
|
*
|
|
*
|
|
|
* This class extends MarkupTemplate to provide a reusable, cached template.
|
|
* This class extends MarkupTemplate to provide a reusable, cached template.
|
|
|
* The HTML is parsed once and cached; new_instance() returns efficient copies.
|
|
* The HTML is parsed once and cached; new_instance() returns efficient copies.
|
|
|
|
|
+ * CSS is served separately via CounterStyles FastResource for optimal caching.
|
|
|
*/
|
|
*/
|
|
|
class CounterTemplate : MarkupTemplate {
|
|
class CounterTemplate : MarkupTemplate {
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Returns the HTML markup for this template.
|
|
/// Returns the HTML markup for this template.
|
|
|
- /// This could also use read_file() to load from disk.
|
|
|
|
|
|
|
+ /// CSS is linked externally via /styles.css for better caching.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
protected override string get_markup() {
|
|
protected override string get_markup() {
|
|
|
return """<!DOCTYPE html>
|
|
return """<!DOCTYPE html>
|
|
@@ -75,148 +223,7 @@ class CounterTemplate : MarkupTemplate {
|
|
|
<meta charset="UTF-8"/>
|
|
<meta charset="UTF-8"/>
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
|
<title>Document Template Example</title>
|
|
<title>Document Template Example</title>
|
|
|
- <style>
|
|
|
|
|
- body {
|
|
|
|
|
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
|
|
- max-width: 700px;
|
|
|
|
|
- margin: 0 auto;
|
|
|
|
|
- padding: 20px;
|
|
|
|
|
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
- min-height: 100vh;
|
|
|
|
|
- }
|
|
|
|
|
- .card {
|
|
|
|
|
- background: white;
|
|
|
|
|
- border-radius: 12px;
|
|
|
|
|
- padding: 25px;
|
|
|
|
|
- margin: 15px 0;
|
|
|
|
|
- box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
|
|
|
|
- }
|
|
|
|
|
- h1 {
|
|
|
|
|
- color: #333;
|
|
|
|
|
- margin-top: 0;
|
|
|
|
|
- }
|
|
|
|
|
- .counter-display {
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- padding: 30px;
|
|
|
|
|
- background: #f8f9fa;
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- margin: 20px 0;
|
|
|
|
|
- }
|
|
|
|
|
- .counter-value {
|
|
|
|
|
- font-size: 72px;
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
- color: #667eea;
|
|
|
|
|
- line-height: 1;
|
|
|
|
|
- }
|
|
|
|
|
- .counter-label {
|
|
|
|
|
- color: #666;
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
- text-transform: uppercase;
|
|
|
|
|
- letter-spacing: 2px;
|
|
|
|
|
- }
|
|
|
|
|
- .button-group {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- gap: 10px;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- margin: 20px 0;
|
|
|
|
|
- }
|
|
|
|
|
- button {
|
|
|
|
|
- padding: 12px 24px;
|
|
|
|
|
- border: none;
|
|
|
|
|
- border-radius: 6px;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- font-size: 16px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- transition: all 0.2s;
|
|
|
|
|
- }
|
|
|
|
|
- button:hover {
|
|
|
|
|
- transform: translateY(-2px);
|
|
|
|
|
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
|
|
|
- }
|
|
|
|
|
- .btn-primary {
|
|
|
|
|
- background: #667eea;
|
|
|
|
|
- color: white;
|
|
|
|
|
- }
|
|
|
|
|
- .btn-primary:hover {
|
|
|
|
|
- background: #5a6fd6;
|
|
|
|
|
- }
|
|
|
|
|
- .btn-danger {
|
|
|
|
|
- background: #e74c3c;
|
|
|
|
|
- color: white;
|
|
|
|
|
- }
|
|
|
|
|
- .btn-danger:hover {
|
|
|
|
|
- background: #c0392b;
|
|
|
|
|
- }
|
|
|
|
|
- .btn-success {
|
|
|
|
|
- background: #2ecc71;
|
|
|
|
|
- color: white;
|
|
|
|
|
- }
|
|
|
|
|
- .btn-success:hover {
|
|
|
|
|
- background: #27ae60;
|
|
|
|
|
- }
|
|
|
|
|
- .info-grid {
|
|
|
|
|
- display: grid;
|
|
|
|
|
- grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
- gap: 15px;
|
|
|
|
|
- margin: 20px 0;
|
|
|
|
|
- }
|
|
|
|
|
- .info-item {
|
|
|
|
|
- background: #f8f9fa;
|
|
|
|
|
- padding: 15px;
|
|
|
|
|
- border-radius: 6px;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- }
|
|
|
|
|
- .info-label {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: #666;
|
|
|
|
|
- text-transform: uppercase;
|
|
|
|
|
- letter-spacing: 1px;
|
|
|
|
|
- }
|
|
|
|
|
- .info-value {
|
|
|
|
|
- font-size: 18px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #333;
|
|
|
|
|
- margin-top: 5px;
|
|
|
|
|
- }
|
|
|
|
|
- .status-positive {
|
|
|
|
|
- color: #2ecc71 !important;
|
|
|
|
|
- }
|
|
|
|
|
- .status-negative {
|
|
|
|
|
- color: #e74c3c !important;
|
|
|
|
|
- }
|
|
|
|
|
- .status-zero {
|
|
|
|
|
- color: #666 !important;
|
|
|
|
|
- }
|
|
|
|
|
- code {
|
|
|
|
|
- background: #e8e8e8;
|
|
|
|
|
- padding: 2px 6px;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
- }
|
|
|
|
|
- pre {
|
|
|
|
|
- background: #263238;
|
|
|
|
|
- color: #aed581;
|
|
|
|
|
- padding: 15px;
|
|
|
|
|
- border-radius: 6px;
|
|
|
|
|
- overflow-x: auto;
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- }
|
|
|
|
|
- .feature-list {
|
|
|
|
|
- margin: 0;
|
|
|
|
|
- padding-left: 20px;
|
|
|
|
|
- }
|
|
|
|
|
- .feature-list li {
|
|
|
|
|
- margin: 8px 0;
|
|
|
|
|
- color: #555;
|
|
|
|
|
- }
|
|
|
|
|
- a {
|
|
|
|
|
- color: #667eea;
|
|
|
|
|
- text-decoration: none;
|
|
|
|
|
- }
|
|
|
|
|
- a:hover {
|
|
|
|
|
- text-decoration: underline;
|
|
|
|
|
- }
|
|
|
|
|
- </style>
|
|
|
|
|
|
|
+ <link rel="stylesheet" href="/styles.css"/>
|
|
|
</head>
|
|
</head>
|
|
|
<body>
|
|
<body>
|
|
|
<div class="card" id="main-card">
|
|
<div class="card" id="main-card">
|
|
@@ -477,6 +484,7 @@ void main(string[] args) {
|
|
|
print("╠══════════════════════════════════════════════════════════════╣\n");
|
|
print("╠══════════════════════════════════════════════════════════════╣\n");
|
|
|
print("║ Endpoints: ║\n");
|
|
print("║ Endpoints: ║\n");
|
|
|
print("║ / - Counter page (template with modifications) ║\n");
|
|
print("║ / - Counter page (template with modifications) ║\n");
|
|
|
|
|
+ print("║ /styles.css - CSS stylesheet (FastResource) ║\n");
|
|
|
print("║ /increment - Increase counter (POST) ║\n");
|
|
print("║ /increment - Increase counter (POST) ║\n");
|
|
|
print("║ /decrement - Decrease counter (POST) ║\n");
|
|
print("║ /decrement - Decrease counter (POST) ║\n");
|
|
|
print("║ /reset - Reset counter (POST) ║\n");
|
|
print("║ /reset - Reset counter (POST) ║\n");
|
|
@@ -496,6 +504,18 @@ void main(string[] args) {
|
|
|
// Endpoints use field initializer injection with Inversion.inject<T>()
|
|
// Endpoints use field initializer injection with Inversion.inject<T>()
|
|
|
application.add_singleton<CounterTemplate>();
|
|
application.add_singleton<CounterTemplate>();
|
|
|
|
|
|
|
|
|
|
+ // Register CSS as a FastResource - pre-compressed, cached in memory
|
|
|
|
|
+ // This demonstrates serving static content efficiently with ETag caching
|
|
|
|
|
+ application.add_singleton_endpoint<FastResource>(new EndpointRoute("/styles.css"), () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return new FastResource.from_string(COUNTER_CSS)
|
|
|
|
|
+ .with_content_type("text/css; charset=utf-8")
|
|
|
|
|
+ .with_default_compressors();
|
|
|
|
|
+ } catch (Error e) {
|
|
|
|
|
+ error("Failed to create CSS resource: %s", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
// Register endpoints
|
|
// Register endpoints
|
|
|
application.add_endpoint<HomePageEndpoint>(new EndpointRoute("/"));
|
|
application.add_endpoint<HomePageEndpoint>(new EndpointRoute("/"));
|
|
|
application.add_endpoint<IncrementEndpoint>(new EndpointRoute("/increment"));
|
|
application.add_endpoint<IncrementEndpoint>(new EndpointRoute("/increment"));
|