decided in favor of dedicated COMPONENT() macro, updates to documentation

This commit is contained in:
udo
2026-04-19 11:34:03 +00:00
parent be514d63d6
commit 2b5586d7df
56 changed files with 926 additions and 401 deletions
@@ -1,6 +1,6 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
String title = first(context.call["title"].to_string(), "Sign in with OAuth");
String subtitle = first(context.call["subtitle"].to_string(), "Choose your preferred authentication method");
@@ -42,7 +42,7 @@ RENDER(Request& context)
</div>
<? services.each([&](DTree service, String service_key) {
String client_id = context.call[service_key + "_client_id"].to_string();
String handler_name = starter_safe_name(service_key);
String handler_name = ascii_safe_name(service_key);
String service_name = service["name"].to_string();
String scope = service["scope"].to_string();
String auth_url = service["auth_url"].to_string();
@@ -63,7 +63,7 @@ RENDER(Request& context)
if (button) button.style.display = 'none';
if (loading) loading.style.display = 'block';
const clientId = <?: starter_json_string(client_id) ?>;
const clientId = <?: json_encode(client_id) ?>;
if (!clientId || clientId.indexOf('YOUR_') === 0) {
alert('<?= service_name ?> OAuth is not configured yet.');
if (button) button.style.display = '';
@@ -74,21 +74,21 @@ RENDER(Request& context)
const state = btoa(Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2)).replace(/[^a-zA-Z0-9]/g, '');
const params = new URLSearchParams({
client_id: clientId,
redirect_uri: <?: starter_json_string(callback_url) ?>,
scope: <?: starter_json_string(scope) ?>,
redirect_uri: <?: json_encode(callback_url) ?>,
scope: <?: json_encode(scope) ?>,
response_type: 'code',
state: state
});
sessionStorage.setItem('oauth_state', state);
sessionStorage.setItem('oauth_service', <?: starter_json_string(service_key) ?>);
sessionStorage.setItem('oauth_service', <?: json_encode(service_key) ?>);
fetch(<?: starter_json_string(store_url) ?>, {
fetch(<?: json_encode(store_url) ?>, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({oauth_service: <?: starter_json_string(service_key) ?>, oauth_state: state})
body: JSON.stringify({oauth_service: <?: json_encode(service_key) ?>, oauth_state: state})
}).catch(console.error).finally(() => {
window.location.href = <?: starter_json_string(auth_url) ?> + '?' + params.toString();
window.location.href = <?: json_encode(auth_url) ?> + '?' + params.toString();
});
};
</script>
@@ -1,6 +1,6 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
@@ -74,7 +74,7 @@ DTree data_format_value(DTree value, DTree row, DTree column)
return(result);
}
RENDER:SUMMARY_METRICS(Request& context)
COMPONENT:SUMMARY_METRICS(Request& context)
{
String title = context.call["title"].to_string();
String subtitle = context.call["subtitle"].to_string();
@@ -103,7 +103,7 @@ RENDER:SUMMARY_METRICS(Request& context)
</>
}
RENDER:TIMESERIES_CHART(Request& context)
COMPONENT:TIMESERIES_CHART(Request& context)
{
starter_register_js("js/u-format.js", context);
starter_register_js("js/u-timeseries-chart.js", context);
@@ -132,21 +132,21 @@ RENDER:TIMESERIES_CHART(Request& context)
<script>
(function () {
if (typeof UTimeSeriesChart === 'undefined') return;
var chart = new UTimeSeriesChart(<?: starter_json_string(canvas_id) ?>, {
xAxisLabel: <?: starter_json_string(x_axis_label) ?>,
yAxisLeftLabel: <?: starter_json_string(y_axis_left_label) ?>,
yAxisRightLabel: <?: starter_json_string(y_axis_right_label) ?>,
yAxisLeftFormat: <?: starter_json_string(y_axis_left_format) ?>,
yAxisRightFormat: <?: starter_json_string(y_axis_right_format) ?>
var chart = new UTimeSeriesChart(<?: json_encode(canvas_id) ?>, {
xAxisLabel: <?: json_encode(x_axis_label) ?>,
yAxisLeftLabel: <?: json_encode(y_axis_left_label) ?>,
yAxisRightLabel: <?: json_encode(y_axis_right_label) ?>,
yAxisLeftFormat: <?: json_encode(y_axis_left_format) ?>,
yAxisRightFormat: <?: json_encode(y_axis_right_format) ?>
});
chart.setData(<?= json_encode(context.call["series"]) ?>, <?= json_encode(context.call["x_labels"]) ?>);
window[<?: starter_json_string("chart_" + chart_id) ?>] = chart;
window[<?: json_encode("chart_" + chart_id) ?>] = chart;
}());
</script>
</>
}
RENDER:SORTABLE_TABLE(Request& context)
COMPONENT:SORTABLE_TABLE(Request& context)
{
starter_register_js("js/u-format.js", context);
starter_register_js("js/u-sortable-table.js", context);
@@ -205,13 +205,13 @@ RENDER:SORTABLE_TABLE(Request& context)
<script>
(function () {
if (typeof USortableTable === 'undefined') return;
USortableTable.init(<?: starter_json_string(table_id) ?>, <?= json_encode(init_options) ?>);
USortableTable.init(<?: json_encode(table_id) ?>, <?= json_encode(init_options) ?>);
}());
</script>
</>
}
RENDER:DATA_TABLE(Request& context)
COMPONENT:DATA_TABLE(Request& context)
{
String table_id = first(context.call["id"].to_string(), "data-grid-" + std::to_string((u64)time()));
starter_register_js("js/ag-grid/ag-grid-community.min.js", context);
@@ -259,15 +259,15 @@ RENDER:DATA_TABLE(Request& context)
(function () {
if (typeof agGrid === 'undefined') return;
const gridOptions = <?= json_encode(grid_options) ?>;
new agGrid.Grid(document.getElementById(<?: starter_json_string(table_id) ?>), gridOptions);
document.getElementById(<?: starter_json_string(table_id + "-search") ?>)?.addEventListener('input', function () {
new agGrid.Grid(document.getElementById(<?: json_encode(table_id) ?>), gridOptions);
document.getElementById(<?: json_encode(table_id + "-search") ?>)?.addEventListener('input', function () {
gridOptions.api.setQuickFilter(this.value);
});
document.getElementById(<?: starter_json_string(table_id + "-clear-filters") ?>)?.addEventListener('click', function () {
document.getElementById(<?: json_encode(table_id + "-clear-filters") ?>)?.addEventListener('click', function () {
gridOptions.api.setFilterModel(null);
gridOptions.api.setQuickFilter('');
});
document.getElementById(<?: starter_json_string(table_id + "-export-csv") ?>)?.addEventListener('click', function () {
document.getElementById(<?: json_encode(table_id + "-export-csv") ?>)?.addEventListener('click', function () {
gridOptions.api.exportDataAsCsv();
});
}());
@@ -42,7 +42,7 @@ void marketing_default_features(DTree& features)
features.push(item);
}
RENDER:HERO_SECTION(Request& context)
COMPONENT:HERO_SECTION(Request& context)
{
String title = first(context.call["title"].to_string(), "Welcome to the Present");
String subtitle = first(context.call["subtitle"].to_string(), "Experience no-quite-modern web development with our cutting-edge framework");
@@ -192,7 +192,7 @@ RENDER:HERO_SECTION(Request& context)
</>
}
RENDER:FEATURES_GRID(Request& context)
COMPONENT:FEATURES_GRID(Request& context)
{
DTree features = context.call["features"];
marketing_default_features(features);
@@ -286,7 +286,7 @@ RENDER:FEATURES_GRID(Request& context)
</>
}
RENDER:STATS_SECTION(Request& context)
COMPONENT:STATS_SECTION(Request& context)
{
DTree stats = context.call["stats"];
if(stats.get_type_name() != "array" || stats["0"]["label"].to_string() == "")
@@ -362,7 +362,7 @@ RENDER:STATS_SECTION(Request& context)
</>
}
RENDER:BRANDS_SHOWCASE(Request& context)
COMPONENT:BRANDS_SHOWCASE(Request& context)
{
DTree logos = context.call["logos"];
if(logos.get_type_name() != "array" || logos["0"]["name"].to_string() == "")
@@ -408,7 +408,7 @@ RENDER:BRANDS_SHOWCASE(Request& context)
</>
}
RENDER:TESTIMONIALS(Request& context)
COMPONENT:TESTIMONIALS(Request& context)
{
DTree testimonials = context.call["testimonials"];
if(testimonials.get_type_name() != "array" || testimonials["0"]["name"].to_string() == "")
@@ -471,7 +471,7 @@ RENDER:TESTIMONIALS(Request& context)
</>
}
RENDER:PRICING_TABLE(Request& context)
COMPONENT:PRICING_TABLE(Request& context)
{
DTree plans = context.call["plans"];
if(plans.get_type_name() != "array" || plans["0"]["name"].to_string() == "")
@@ -544,7 +544,7 @@ RENDER:PRICING_TABLE(Request& context)
</>
}
RENDER:CTA_SECTION(Request& context)
COMPONENT:CTA_SECTION(Request& context)
{
String title = first(context.call["title"].to_string(), "Ready to Get Started?");
String subtitle = first(context.call["subtitle"].to_string(), "Join thousands of developers building amazing applications with our framework.");
@@ -1,10 +1,10 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
String current_theme = starter_theme_value("key", context);
String current_label = first(starter_theme_value("label", context), current_theme);
String current_theme = context.cfg.get_by_path("theme/key").to_string();
String current_label = first(context.cfg.get_by_path("theme/label").to_string(), current_theme);
String route_path = context.var["starter"]["route"]["l_path"].to_string();
<>
@@ -108,7 +108,7 @@ RENDER(Request& context)
<div id="theme-menu" class="theme-menu" hidden>
<div class="theme-menu-title">Available Themes</div>
<div class="theme-menu-list">
<? starter_cfg("theme/options", context).each([&](DTree theme_info, String theme_key) {
<? context.cfg.get_by_path("theme/options").each([&](DTree theme_info, String theme_key) {
StringMap params;
params["theme"] = theme_key;
String href = starter_link(route_path == "index" ? "" : route_path, params, context);
+95
View File
@@ -0,0 +1,95 @@
.horizontal .progressbar-item {
display: flex;
min-width: 100%;
padding: 5px;
}
.progressbar-container.vertical {
display: flex;
}
.vertical .progressbar-item {
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 5px;
text-align: center;
}
.progressbar-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: bold;
}
.horizontal .progressbar-label, .horizontal .progressbar-value {
flex: 0 0 auto;
margin-right: 10px;
}
.horizontal .progressbar-label {
min-width: 50px;
}
.vertical .progressbar-label {
text-align: center;
}
.horizontal .progressbar-value {
min-width: 50px;
text-align: right;
}
.progressbar-background {
flex: 1;
background: var(--bg-color);
padding: 4px;
display: flex;
}
.vertical .progressbar-background {
flex-direction: column;
justify-content: flex-end;
}
.progressbar-bar {
transition: all 0.3s ease;
}
.progressbar-marker {
position: absolute;
z-index: 10;
opacity: 0.5;
background: var(--text-color, #333);
}
.progressbar-marker:hover {
opacity: 1;
}
.horizontal .progressbar-marker {
border-width: 0 2px 0 2px;
height: 100%;
top: 0;
min-width: 4px;
}
.vertical .progressbar-marker {
border-width: 2px 0 2px 0;
width: 100%;
left: 0;
min-height: 4px;
}
.progressbar-background {
position: relative;
}
.needlegauge-svg .needle {
transition: all 0.3s ease;
}
.needlegauge-item {
display: inline-block;
}
+121
View File
@@ -0,0 +1,121 @@
// common code for gauges components
window.GaugeComponents = window.GaugeComponents || {};
Object.assign(window.GaugeComponents, { // as a namespace
// utility functions
clampValue: function(value, min, max) {
return Math.min(max, Math.max(min, value));
},
pickEntryFromRange: function(ranges, value) {
if (!Array.isArray(ranges)) return null;
for (const entry of ranges) {
if (value >= entry.from && value <= entry.to) return entry;
}
return null;
},
/**
* Creates an SVG element with namespace
*/
createSVGElement: function(tagName, attributes = {}) {
const element = document.createElementNS('http://www.w3.org/2000/svg', tagName);
Object.entries(attributes).forEach(([key, value]) => {
element.setAttribute(key, value);
});
return element;
},
/**
* Gets CSS custom property value from computed styles
*/
getCSSVar: function(varName) {
return getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
},
resolveColor: function(colorSpec, value, pct) {
if (Array.isArray(colorSpec)) {
const match = this.pickEntryFromRange(colorSpec, value);
if (match && match.color) return match.color;
}
if (typeof colorSpec === 'string' && colorSpec !== '') return colorSpec;
if (pct < 60) return this.getCSSVar('--success') || '#10b981';
if (pct < 85) return this.getCSSVar('--warning') || '#f59e0b';
return this.getCSSVar('--error') || '#ef4444';
},
formatValue: function(value, precision, suffix) {
const numericValue = Number(value);
const normalizedPrecision = Number.isFinite(precision) ? precision : 1;
if (!Number.isFinite(numericValue)) return '--';
return numericValue.toFixed(normalizedPrecision) + (suffix || '');
},
gaugeArcPoint: function(pct, radius = 50) {
const angle = Math.PI - (pct / 100) * Math.PI;
return {
x: 60 + radius * Math.cos(angle),
y: 60 - radius * Math.sin(angle),
};
},
updateWatermark: function(prefix, pct) {
const now = Date.now();
this._watermarks = this._watermarks || {};
let watermark = this._watermarks[prefix];
const resetWindow = 10 * 60 * 1000;
if (!watermark || (now - watermark.resetTs) > resetWindow) {
watermark = { lo: pct, hi: pct, resetTs: now };
this._watermarks[prefix] = watermark;
} else {
if (pct < watermark.lo) watermark.lo = pct;
if (pct > watermark.hi) watermark.hi = pct;
}
return watermark;
},
renderWatermarkTick: function(lineId, pct) {
const line = document.getElementById(lineId);
if (!line) return;
if (pct == null) {
line.setAttribute('opacity', '0');
return;
}
const outer = this.gaugeArcPoint(pct, 53);
const inner = this.gaugeArcPoint(pct, 43);
line.setAttribute('x1', outer.x.toFixed(1));
line.setAttribute('y1', outer.y.toFixed(1));
line.setAttribute('x2', inner.x.toFixed(1));
line.setAttribute('y2', inner.y.toFixed(1));
line.setAttribute('opacity', '0.7');
},
updateArcGauge: function(options) {
const value = Number(options.value);
const max = Number(options.max || 100);
const normalizedValue = Number.isFinite(value) ? value : 0;
const pct = this.clampValue(max === 0 ? 0 : (normalizedValue / max) * 100, 0, 100);
const arcLength = (pct / 100) * 157.08;
const arc = document.getElementById(options.arcId);
const text = document.getElementById(options.textId);
const meta = options.metaId ? document.getElementById(options.metaId) : null;
if (arc) {
arc.setAttribute('stroke-dasharray', arcLength.toFixed(1) + ' 157.08');
arc.setAttribute('stroke', this.resolveColor(options.color, normalizedValue, pct));
}
if (text) {
text.textContent = this.formatValue(normalizedValue, options.precision, options.suffix);
}
if (meta && options.meta != null) {
meta.textContent = options.meta;
}
if (options.watermarkPrefix) {
const watermark = this.updateWatermark(options.watermarkPrefix, pct);
this.renderWatermarkTick(options.watermarkPrefix + 'WmLo', watermark.lo);
this.renderWatermarkTick(options.watermarkPrefix + 'WmHi', watermark.hi);
}
}
});
@@ -1,6 +1,6 @@
#load "../../lib/app.uce"
RENDER:PROGRESSBAR(Request& context)
COMPONENT:PROGRESSBAR(Request& context)
{
starter_register_js("components/gauges/common.js", context);
starter_register_css("themes/common/css/gauges.css", context);
@@ -96,13 +96,13 @@ RENDER:PROGRESSBAR(Request& context)
});
}
};
ProgressbarComponents.start_listen(<?= json_encode(context.call) ?>);
ProgressbarComponents.start_listen(<?: json_encode(context.call) ?>);
</script>
<? } ?>
</>
}
RENDER:NEEDLEGAUGE(Request& context)
COMPONENT:NEEDLEGAUGE(Request& context)
{
starter_register_css("themes/common/css/gauges.css", context);
String gauge_id = first(context.call["id"].to_string(), "needlegauge-" + std::to_string((u64)time()));
@@ -167,7 +167,7 @@ RENDER:NEEDLEGAUGE(Request& context)
</>
}
RENDER:ARCGAUGE(Request& context)
COMPONENT:ARCGAUGE(Request& context)
{
starter_register_js("components/gauges/common.js", context);
starter_register_css("themes/common/css/gauges.css", context);
@@ -1,13 +1,15 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
DTree user = starter_current_user(context);
StarterUser users(context);
DTree user = users.current();
bool signed_in = user["email"].to_string() != "";
String wrapper_class = starter_theme_value("key", context) == "localfirst" ? "admin-account-card" : "nav-account nav-menu";
String links_class = starter_theme_value("key", context) == "localfirst" ? "admin-account-links" : "";
String name_class = starter_theme_value("key", context) == "localfirst" ? "admin-account-name" : "account-name";
String theme_key = context.cfg.get_by_path("theme/key").to_string();
String wrapper_class = theme_key == "localfirst" ? "admin-account-card" : "nav-account nav-menu";
String links_class = theme_key == "localfirst" ? "admin-account-links" : "";
String name_class = theme_key == "localfirst" ? "admin-account-name" : "account-name";
<>
<div class="<?= wrapper_class ?>">
@@ -20,7 +22,7 @@ RENDER(Request& context)
<? } else { ?>
<? if(links_class != "") { ?><div class="<?= links_class ?>"><? } ?>
<a href="<?= starter_link("account/login", context) ?>">Login</a>
<? if(starter_cfg("users/enable_signup", context).to_string() != "") { ?>
<? if(context.cfg.get_by_path("users/enable_signup").to_string() != "") { ?>
<a href="<?= starter_link("account/register", context) ?>">Register</a>
<? } ?>
<? if(links_class != "") { ?></div><? } ?>
@@ -1,13 +1,13 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
if(context.var["starter"]["embed_mode"].to_string() != "")
return;
String text = first(starter_theme_value("footer_text", context), starter_cfg_string("site/name", context));
String inner_class = starter_theme_value("key", context) == "portal-light" ? "footer-inner" : "";
String text = first(context.cfg.get_by_path("theme/footer_text").to_string(), context.cfg.get_by_path("site/name").to_string());
String inner_class = context.cfg.get_by_path("theme/key").to_string() == "portal-light" ? "footer-inner" : "";
<>
<footer>
@@ -1,6 +1,6 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
@@ -29,7 +29,7 @@ RENDER(Request& context)
return;
}
String theme_key = starter_theme_value("key", context);
String theme_key = context.cfg.get_by_path("theme/key").to_string();
bool embed_mode = context.var["starter"]["embed_mode"].to_string() == "1";
String body_class = embed_mode ? "embed-mode" : "";
if(theme_key == "portal-dark")
@@ -43,9 +43,9 @@ RENDER(Request& context)
<>
<!doctype html>
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= starter_theme_value("key", context) ?>">
<html class="<?= starter_html_class(context) ?>" lang="en" data-theme-key="<?= context.cfg.get_by_path("theme/key").to_string() ?>">
<head>
<? render_component("page_shell:HEAD", context.call, context); ?>
<? render_component(":HEAD", context.call, context); ?>
</head>
<body<?: body_class != "" ? " class=\"" + html_escape(body_class) + "\"" : "" ?>>
<? if(theme_key == "retro-gaming") { ?><div class="retro-stars"></div><? } ?>
@@ -61,10 +61,10 @@ RENDER(Request& context)
<nav class="admin-nav">
<div class="admin-nav-header">
<a class="admin-nav-title" href="<?= starter_link("", context) ?>">
<img class="admin-nav-logo-img" src="<?= starter_asset_url("themes/localfirst/img/local_first_logo.png", context) ?>" alt="<?= starter_cfg_string("site/name", context) ?>" />
<img class="admin-nav-logo-img" src="<?= starter_asset_url("themes/localfirst/img/local_first_logo.png", context) ?>" alt="<?= context.cfg.get_by_path("site/name").to_string() ?>" />
</a>
</div>
<? starter_cfg("menu", context).each([&](DTree menu_item, String menu_key) {
<? context.cfg.get_by_path("menu").each([&](DTree menu_item, String menu_key) {
if(menu_item["hidden"].to_string() == "1")
return;
String current_path = context.var["starter"]["route"]["l_path"].to_string();
@@ -95,17 +95,17 @@ RENDER(Request& context)
</>
}
RENDER:HEAD(Request& context)
COMPONENT:HEAD(Request& context)
{
starter_boot(context);
String title = first(context.var["starter"]["page_title"].to_string(), starter_cfg_string("site/default_page_title", context));
String description = first(starter_theme_value("meta_description", context), "UCE starter example");
String theme_color = first(starter_theme_value("theme_color", context), "#0f172a");
String icon = starter_asset_url(starter_theme_value("path", context) + "icon.png", context);
String title = first(context.var["starter"]["page_title"].to_string(), context.cfg.get_by_path("site/default_page_title").to_string());
String description = first(context.cfg.get_by_path("theme/meta_description").to_string(), "UCE starter example");
String theme_color = first(context.cfg.get_by_path("theme/theme_color").to_string(), "#0f172a");
String icon = starter_asset_url(context.cfg.get_by_path("theme/path").to_string() + "icon.png", context);
<>
<meta charset="utf-8">
<title><?= title + " | " + starter_cfg_string("site/name", context) ?></title>
<title><?= title + " | " + context.cfg.get_by_path("site/name").to_string() ?></title>
<meta name="description" content="<?= description ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="<?= theme_color ?>">
@@ -1,14 +1,14 @@
#load "../../lib/app.uce"
RENDER(Request& context)
COMPONENT(Request& context)
{
starter_boot(context);
<>
<nav<?: starter_theme_value("key", context) == "portal-dark" || starter_theme_value("key", context) == "dark" || starter_theme_value("key", context) == "retro-gaming" ? " class=\"nav-shell\"" : "" ?>>
<nav<?: context.cfg.get_by_path("theme/key").to_string() == "portal-dark" || context.cfg.get_by_path("theme/key").to_string() == "dark" || context.cfg.get_by_path("theme/key").to_string() == "retro-gaming" ? " class=\"nav-shell\"" : "" ?>>
<div class="nav-menu">
<a href="<?= starter_link("", context) ?>"><?= starter_cfg_string("site/name", context) ?></a>
<? starter_cfg("menu", context).each([&](DTree menu_item, String menu_key) {
<a href="<?= starter_link("", context) ?>"><?= context.cfg.get_by_path("site/name").to_string() ?></a>
<? context.cfg.get_by_path("menu").each([&](DTree menu_item, String menu_key) {
if(menu_item["hidden"].to_string() == "1")
return;
?><a href="<?= starter_menu_href(menu_key, menu_item, context) ?>"><?= menu_item["title"].to_string() ?></a><?
@@ -1,6 +1,6 @@
#load "../../lib/app.uce"
RENDER:APP_FRAME(Request& context)
COMPONENT:APP_FRAME(Request& context)
{
starter_register_css("themes/common/css/workspace.css", context);
starter_register_js("js/u-workspace-shell.js", context);
@@ -16,7 +16,7 @@ RENDER:APP_FRAME(Request& context)
</>
}
RENDER:EMPTY_STATE(Request& context)
COMPONENT:EMPTY_STATE(Request& context)
{
<>
<div class="ws-empty-state<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
@@ -28,14 +28,14 @@ RENDER:EMPTY_STATE(Request& context)
</>
}
RENDER:LIST_STATE(Request& context)
COMPONENT:LIST_STATE(Request& context)
{
<>
<div class="ws-list-state<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>"><? if(context.call["icon_class"].to_string() != "") { ?><i class="<?= context.call["icon_class"].to_string() ?>"></i><? } ?><span><?= context.call["text"].to_string() ?></span></div>
</>
}
RENDER:MOBILE_BAR(Request& context)
COMPONENT:MOBILE_BAR(Request& context)
{
<>
<div class="ws-mobile-bar<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
@@ -47,7 +47,7 @@ RENDER:MOBILE_BAR(Request& context)
</>
}
RENDER:PANEL_HEADER(Request& context)
COMPONENT:PANEL_HEADER(Request& context)
{
<>
<div class="ws-panel-header<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
@@ -60,7 +60,7 @@ RENDER:PANEL_HEADER(Request& context)
</>
}
RENDER:PANEL(Request& context)
COMPONENT:PANEL(Request& context)
{
<>
<section<?: context.call["id"].to_string() != "" ? " id=\"" + html_escape(context.call["id"].to_string()) + "\"" : "" ?> class="ws-panel<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
@@ -70,7 +70,7 @@ RENDER:PANEL(Request& context)
</>
}
RENDER:SECTION_HEAD(Request& context)
COMPONENT:SECTION_HEAD(Request& context)
{
<>
<div class="ws-section-head<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
@@ -80,7 +80,7 @@ RENDER:SECTION_HEAD(Request& context)
</>
}
RENDER:SECTION(Request& context)
COMPONENT:SECTION(Request& context)
{
<>
<section<?: context.call["id"].to_string() != "" ? " id=\"" + html_escape(context.call["id"].to_string()) + "\"" : "" ?> class="ws-section<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
@@ -90,7 +90,7 @@ RENDER:SECTION(Request& context)
</>
}
RENDER:SIDEBAR_SHELL(Request& context)
COMPONENT:SIDEBAR_SHELL(Request& context)
{
<>
<aside<?: context.call["id"].to_string() != "" ? " id=\"" + html_escape(context.call["id"].to_string()) + "\"" : "" ?> class="ws-sidebar<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
@@ -100,7 +100,7 @@ RENDER:SIDEBAR_SHELL(Request& context)
</>
}
RENDER:SIDEBAR_TOOLBAR(Request& context)
COMPONENT:SIDEBAR_TOOLBAR(Request& context)
{
<>
<div class="ws-sidebar-top<?= context.call["class"].to_string() != "" ? " " + html_escape(context.call["class"].to_string()) : "" ?>">
@@ -113,7 +113,7 @@ RENDER:SIDEBAR_TOOLBAR(Request& context)
</>
}
RENDER:STATUS_PILL(Request& context)
COMPONENT:STATUS_PILL(Request& context)
{
String variant = to_lower(first(context.call["variant"].to_string(), "neutral"));
<>
+12 -250
View File
@@ -1,49 +1,4 @@
String starter_dirname(String path)
{
if(path == "")
return("");
auto parts = split(path, "/");
if(parts.size() <= 1)
return("");
parts.pop_back();
String result = join(parts, "/");
if(result == "")
return("/");
return(result);
}
String starter_fs_join(String root, String relative)
{
if(relative == "")
return(root);
if(relative[0] == '/')
return(relative);
if(root == "" || root == "/")
return(root + relative);
if(root[root.length() - 1] == '/')
return(root + relative);
return(root + "/" + relative);
}
String starter_safe_name(String raw)
{
String result = "";
for(auto c : raw)
{
if((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))
result.append(1, c);
else
result.append(1, '_');
}
return(result);
}
String starter_json_string(String raw)
{
DTree value;
value = raw;
return(json_encode(value));
}
#load "user.class.h"
String starter_fs_root(Request& context)
{
@@ -66,7 +21,7 @@ String starter_base_url(Request& context)
String base = context.var["starter"]["base_url"].to_string();
if(base == "")
{
base = starter_dirname(starter_script_url(context));
base = dirname(starter_script_url(context));
if(base == "")
base = "/";
if(base[base.length() - 1] != '/')
@@ -77,34 +32,12 @@ String starter_base_url(Request& context)
String starter_fs_path(String relative, Request& context)
{
return(starter_fs_join(starter_fs_root(context), relative));
}
DTree starter_cfg(String path, Request& context)
{
DTree result = context.var["starter"]["config"];
for(auto segment : split(path, "/"))
{
if(segment == "")
continue;
result = result[segment];
}
return(result);
}
String starter_cfg_string(String path, Request& context)
{
return(starter_cfg(path, context).to_string());
return(path_join(starter_fs_root(context), relative));
}
String starter_link(String path, Request& context);
String starter_link(String path, StringMap params, Request& context);
String starter_theme_value(String key, Request& context)
{
return(starter_cfg("theme/" + key, context).to_string());
}
String starter_first_route_segment(Request& context)
{
String query = context.params["QUERY_STRING"];
@@ -251,12 +184,6 @@ DTree starter_default_config()
return(config);
}
void starter_set_status(Request& context, s32 code, String reason)
{
context.response_code = "HTTP/1.1 " + std::to_string(code) + " " + reason;
context.flags.status = code;
}
void starter_register_css(String path, Request& context)
{
context.var["starter"]["assets"]["css"][path] = path;
@@ -321,182 +248,15 @@ String starter_link(String path, StringMap params, Request& context)
return(url);
}
String starter_filebase_root(Request& context)
{
String root = starter_cfg_string("filebase/path", context);
if(root == "")
root = "/tmp/uce-starter-data";
return(root);
}
String starter_hash_id(String raw)
{
raw = to_lower(trim(raw));
return(gen_sha1("uce-starter:" + raw).substr(0, 12));
}
String starter_user_dir(String email, Request& context)
{
String bucket = starter_hash_id(email);
return(starter_filebase_root(context) + "/users/" + bucket.substr(0, 2) + "/" + bucket);
}
String starter_user_file(String email, Request& context)
{
return(starter_user_dir(email, context) + "/account.json");
}
String starter_password_hash(String password, String salt)
{
String hash = "uce-starter:" + password + ":" + salt;
for(s32 i = 0; i < 2048; i++)
hash = gen_sha1(hash + ":" + std::to_string(i));
return(hash);
}
DTree starter_read_json_file(String file_name)
{
DTree result;
if(!file_exists(file_name))
return(result);
String raw = trim(file_get_contents(file_name));
if(raw == "")
return(result);
return(json_decode(raw));
}
bool starter_write_json_file(String file_name, DTree data)
{
mkdir(starter_dirname(file_name));
return(file_put_contents(file_name, json_encode(data)));
}
DTree starter_user_load(String email, Request& context)
{
DTree user = starter_read_json_file(starter_user_file(email, context));
if(user.get_type_name() != "array")
return(DTree());
user["id"] = to_lower(trim(email));
return(user);
}
DTree starter_user_create(String email, String password, Request& context)
{
DTree result;
email = to_lower(trim(email));
password = trim(password);
result["message"] = "";
result["result"].set_bool(false);
if(email == "" || password == "")
{
result["message"] = "email_and_password_required";
return(result);
}
if(email.find("@") == String::npos)
{
result["message"] = "invalid_email";
return(result);
}
DTree existing = starter_user_load(email, context);
if(existing.get_type_name() == "array" && existing["email"].to_string() != "")
{
result["message"] = "user_exists";
return(result);
}
String salt = gen_sha1(std::to_string((u64)time()) + ":" + std::to_string((u64)(microtime() * 1000000.0)) + ":" + make_session_id()).substr(0, 24);
DTree user;
user["email"] = email;
user["salt"] = salt;
user["password_hash"] = starter_password_hash(password, salt);
user["created"] = std::to_string((u64)time());
DTree role;
role = "user";
user["roles"].push(role);
starter_write_json_file(starter_user_file(email, context), user);
user["id"] = email;
result["result"].set_bool(true);
result["id"] = email;
result["profile"] = user;
return(result);
}
DTree starter_user_sign_in(String email, String password, Request& context)
{
DTree result;
result["result"].set_bool(false);
email = to_lower(trim(email));
password = trim(password);
DTree user = starter_user_load(email, context);
if(user.get_type_name() != "array" || user["email"].to_string() == "")
{
result["message"] = "no_such_user";
return(result);
}
if(user["password_hash"].to_string() == "")
{
result["message"] = "no_password_set";
return(result);
}
String expected = starter_password_hash(password, user["salt"].to_string());
if(expected != user["password_hash"].to_string())
{
result["message"] = "invalid_password";
return(result);
}
session_start();
context.session["starter_user_id"] = email;
context.var["starter"]["current_user"] = user;
result["result"].set_bool(true);
result["profile"] = user;
return(result);
}
bool starter_is_signed_in(Request& context)
{
if(context.var["starter"]["current_user"]["email"].to_string() != "")
return(true);
String user_id = context.session["starter_user_id"];
if(user_id == "")
return(false);
DTree user = starter_user_load(user_id, context);
if(user["email"].to_string() == "")
{
context.session.erase("starter_user_id");
return(false);
}
context.var["starter"]["current_user"] = user;
return(true);
}
DTree starter_current_user(Request& context)
{
if(starter_is_signed_in(context))
return(context.var["starter"]["current_user"]);
return(DTree());
}
void starter_user_logout(Request& context)
{
context.session.erase("starter_user_id");
context.var["starter"]["current_user"].clear();
}
void starter_redirect(String path, Request& context)
{
starter_set_status(context, 302, "Found");
context.set_status(302, "Found");
context.header["Location"] = starter_link(path, context);
}
void starter_not_found(String message, Request& context)
{
starter_set_status(context, 404, "Not Found");
context.set_status(404, "Not Found");
context.var["starter"]["error"] = message;
context.var["starter"]["page_title"] = "404 Not Found";
}
@@ -511,7 +271,7 @@ String starter_menu_href(String menu_key, DTree menu_item, Request& context)
String starter_html_class(Request& context)
{
String classes = "no-js";
if(starter_theme_value("mode", context) == "dark")
if(context.cfg.get_by_path("theme/mode").to_string() == "dark")
classes += " dark-theme";
return(classes);
}
@@ -525,7 +285,7 @@ void starter_boot(Request& context)
context.var["starter"]["fs_root"] = get_cwd();
context.var["starter"]["script_url"] = first(context.params["DOCUMENT_URI"], context.params["SCRIPT_NAME"]);
String base_url = starter_dirname(context.var["starter"]["script_url"].to_string());
String base_url = dirname(context.var["starter"]["script_url"].to_string());
if(base_url == "")
base_url = "/";
if(base_url[base_url.length() - 1] != '/')
@@ -541,7 +301,9 @@ void starter_boot(Request& context)
config["theme"][key] = item;
});
config["theme"]["key"] = requested_theme;
context.var["starter"]["config"] = config;
context.cfg = config;
context.var["cfg"].set_reference(&context.cfg);
context.var["starter"]["config"].set_reference(&context.cfg);
context.var["starter"]["route"] = starter_make_route(context);
context.var["starter"]["page_type"] = "html";
context.var["starter"]["page_title"] = first(config["menu"][context.var["starter"]["route"]["l_path"].to_string()]["title"].to_string(), config["site"]["default_page_title"].to_string(), "Home");
@@ -554,9 +316,9 @@ void starter_boot(Request& context)
}
session_start();
starter_is_signed_in(context);
StarterUser(context).is_signed_in();
starter_register_css(starter_theme_value("path", context) + "css/style.css", context);
starter_register_css(context.cfg.get_by_path("theme/path").to_string() + "css/style.css", context);
starter_register_css("themes/common/fontawesome/css/all.min.css", context);
starter_register_js("js/u-query.js", context);
starter_register_js("js/morphdom.js", context);
+186
View File
@@ -0,0 +1,186 @@
#pragma once
class StarterUser
{
public:
Request& context;
explicit StarterUser(Request& request)
: context(request)
{
}
static String session_key()
{
return("starter_user_id");
}
static String normalize_email(String email)
{
return(to_lower(trim(email)));
}
static String hash_id(String raw)
{
raw = normalize_email(raw);
return(gen_sha1("uce-starter:" + raw).substr(0, 12));
}
String root_path()
{
return(first(context.cfg.get_by_path("filebase/path").to_string(), "/tmp/uce-starter-data"));
}
String dir(String email)
{
String bucket = hash_id(email);
return(path_join(root_path(), "users/" + bucket.substr(0, 2) + "/" + bucket));
}
String file_name(String email)
{
return(path_join(dir(email), "account.json"));
}
static String password_hash(String password, String salt)
{
String hash = "uce-starter:" + password + ":" + salt;
for(s32 i = 0; i < 2048; i++)
hash = gen_sha1(hash + ":" + std::to_string(i));
return(hash);
}
static DTree read_json_file(String file_name)
{
DTree result;
if(!file_exists(file_name))
return(result);
String raw = trim(file_get_contents(file_name));
if(raw == "")
return(result);
return(json_decode(raw));
}
static bool write_json_file(String file_name, DTree data)
{
mkdir(dirname(file_name));
return(file_put_contents(file_name, json_encode(data)));
}
DTree load(String email)
{
DTree user = read_json_file(file_name(email));
if(user.get_type_name() != "array")
return(DTree());
user["id"] = normalize_email(email);
return(user);
}
DTree create(String email, String password)
{
DTree result;
email = normalize_email(email);
password = trim(password);
result["message"] = "";
result["result"].set_bool(false);
if(email == "" || password == "")
{
result["message"] = "email_and_password_required";
return(result);
}
if(email.find("@") == String::npos)
{
result["message"] = "invalid_email";
return(result);
}
DTree existing = load(email);
if(existing.get_type_name() == "array" && existing["email"].to_string() != "")
{
result["message"] = "user_exists";
return(result);
}
String salt = gen_sha1(std::to_string((u64)time()) + ":" + std::to_string((u64)(microtime() * 1000000.0)) + ":" + make_session_id()).substr(0, 24);
DTree user;
user["email"] = email;
user["salt"] = salt;
user["password_hash"] = password_hash(password, salt);
user["created"] = std::to_string((u64)time());
DTree role;
role = "user";
user["roles"].push(role);
write_json_file(file_name(email), user);
user["id"] = email;
result["result"].set_bool(true);
result["id"] = email;
result["profile"] = user;
return(result);
}
DTree sign_in(String email, String password)
{
DTree result;
result["result"].set_bool(false);
email = normalize_email(email);
password = trim(password);
DTree user = load(email);
if(user.get_type_name() != "array" || user["email"].to_string() == "")
{
result["message"] = "no_such_user";
return(result);
}
if(user["password_hash"].to_string() == "")
{
result["message"] = "no_password_set";
return(result);
}
String expected = password_hash(password, user["salt"].to_string());
if(expected != user["password_hash"].to_string())
{
result["message"] = "invalid_password";
return(result);
}
session_start();
context.session[session_key()] = email;
context.var["starter"]["current_user"] = user;
result["result"].set_bool(true);
result["profile"] = user;
return(result);
}
bool is_signed_in()
{
if(context.var["starter"]["current_user"]["email"].to_string() != "")
return(true);
String user_id = context.session[session_key()];
if(user_id == "")
return(false);
DTree user = load(user_id);
if(user["email"].to_string() == "")
{
context.session.erase(session_key());
return(false);
}
context.var["starter"]["current_user"] = user;
return(true);
}
DTree current()
{
if(is_signed_in())
return(context.var["starter"]["current_user"]);
return(DTree());
}
void logout()
{
context.session.erase(session_key());
context.var["starter"]["current_user"].clear();
}
};
@@ -4,10 +4,11 @@ RENDER(Request& context)
{
starter_boot(context);
context.var["starter"]["page_title"] = "Login";
StarterUser users(context);
DTree result;
if(context.params["REQUEST_METHOD"] == "POST")
{
result = starter_user_sign_in(context.post["email"], context.post["password"], context);
result = users.sign_in(context.post["email"], context.post["password"]);
if(result["result"].to_string() != "")
{
starter_redirect("account/profile", context);
@@ -3,6 +3,6 @@
RENDER(Request& context)
{
starter_boot(context);
starter_user_logout(context);
StarterUser(context).logout();
starter_redirect("account/login", context);
}
@@ -3,13 +3,14 @@
RENDER(Request& context)
{
starter_boot(context);
if(!starter_is_signed_in(context))
StarterUser users(context);
if(!users.is_signed_in())
{
starter_redirect("account/login", context);
return;
}
context.var["starter"]["page_title"] = "Profile";
DTree user = starter_current_user(context);
DTree user = users.current();
String roles = "";
user["roles"].each([&](DTree role, String key) {
if(roles != "")
@@ -4,9 +4,10 @@ RENDER(Request& context)
{
starter_boot(context);
context.var["starter"]["page_title"] = "Register";
StarterUser users(context);
DTree result;
if(context.params["REQUEST_METHOD"] == "POST")
result = starter_user_create(context.post["email"], context.post["password"], context);
result = users.create(context.post["email"], context.post["password"]);
<>
<h1>Register</h1>
@@ -24,10 +24,10 @@ RENDER(Request& context)
</div>
<div class="card">
<h2>Current Session Status</h2>
<? if(context.session["starter_user_id"] != "") { ?>
<? if(context.session[StarterUser::session_key()] != "") { ?>
<div class="success-card">
<h3>Logged In</h3>
<p>User ID: <?= context.session["starter_user_id"] ?></p>
<p>User ID: <?= context.session[StarterUser::session_key()] ?></p>
</div>
<? } else { ?>
<div class="component-card">
@@ -14,7 +14,7 @@ RENDER(Request& context)
}
else
{
starter_set_status(context, 400, "Bad Request");
context.set_status(400, "Bad Request");
context.var["starter"]["json"]["status"] = "error";
context.var["starter"]["json"]["message"] = "Invalid input";
}
+1 -1
View File
@@ -71,7 +71,7 @@ RENDER(Request& context)
<h2>Theme Families</h2>
<p>Compare the starters built-in theme families and jump to the live gallery.</p>
<div class="theme-grid">
<? starter_cfg("theme/options", context).each([&](DTree theme, String key) {
<? context.cfg.get_by_path("theme/options").each([&](DTree theme, String key) {
if(key != "portal-light" && key != "portal-dark" && key != "localfirst")
return;
StringMap theme_params;
+1 -1
View File
@@ -18,7 +18,7 @@ RENDER(Request& context)
<div class="window-dot green"></div>
<span class="mono-text">marketing_blocks.uce</span>
</div>
<pre class="code-block"><code>RENDER:HERO_SECTION(Request&amp; context)
<pre class="code-block"><code>COMPONENT:HERO_SECTION(Request&amp; context)
{
String title = first(context.call["title"].to_string(), "Welcome");
&lt;&gt;
+10 -1
View File
@@ -10,7 +10,16 @@ RENDER(Request& context)
<p>This is the content of Page 2. You can add more information here.</p>
<p>Sed at dolor leo. Morbi a tellus sed nisl dictum ultricies sit amet at purus. Nam mattis metus sed nunc egestas convallis.</p>
<br/>
<button onclick="$('#page2-section1').load('<?= starter_link("page2-section1", context) ?>')">Load new text</button>
<button id="page2-load-button" data-load-url="<?= starter_link("page2-section1", context) ?>" type="button">Load new text</button>
</div>
<script>
(function () {
const button = document.getElementById('page2-load-button');
if (!button) return;
button.addEventListener('click', function () {
$('#page2-section1').load(button.dataset.loadUrl);
});
}());
</script>
</>
}
@@ -5,9 +5,9 @@ RENDER(Request& context)
starter_boot(context);
context.var["starter"]["page_title"] = "Theme Preview";
starter_register_css("views/themes.css", context);
String current_theme_key = starter_theme_value("key", context);
String theme_label = first(starter_theme_value("label", context), current_theme_key);
String theme_mode = starter_theme_value("mode", context);
String current_theme_key = context.cfg.get_by_path("theme/key").to_string();
String theme_label = first(context.cfg.get_by_path("theme/label").to_string(), current_theme_key);
String theme_mode = context.cfg.get_by_path("theme/mode").to_string();
StringMap theme_params;
theme_params["theme"] = current_theme_key;
+2 -2
View File
@@ -5,7 +5,7 @@ RENDER(Request& context)
starter_boot(context);
context.var["starter"]["page_title"] = "Themes";
starter_register_css("views/themes.css", context);
String current_theme = starter_theme_value("key", context);
String current_theme = context.cfg.get_by_path("theme/key").to_string();
<>
<div class="theme-gallery-shell">
@@ -15,7 +15,7 @@ RENDER(Request& context)
<p>The gallery renders the same preview content in each theme family. This makes it easier to judge shell fit, typography, token balance, and embed behavior.</p>
</section>
<div class="theme-gallery-grid">
<? starter_cfg("theme/options", context).each([&](DTree theme_info, String theme_key) {
<? context.cfg.get_by_path("theme/options").each([&](DTree theme_info, String theme_key) {
StringMap params;
params["theme"] = theme_key;
String preview_href = starter_link("theme-preview", params, context);