diff --git a/examples/web/ui_demo b/examples/web/ui_demo new file mode 100755 index 00000000..38b52e5f Binary files /dev/null and b/examples/web/ui_demo differ diff --git a/examples/web/ui_demo.v b/examples/web/ui_demo.v new file mode 100644 index 00000000..47c7ef05 --- /dev/null +++ b/examples/web/ui_demo.v @@ -0,0 +1,13 @@ +module main + +import freeflowuniverse.herolib.web.ui + +fn main() { + println('Starting UI demo server on port 8080...') + println('Visit http://localhost:8080 to see the admin interface') + + ui.start( + title: 'Demo Admin Panel' + port: 8080 + )! +} \ No newline at end of file diff --git a/examples/web/ui_demo1 b/examples/web/ui_demo1 new file mode 100755 index 00000000..1aa163ff Binary files /dev/null and b/examples/web/ui_demo1 differ diff --git a/examples/web/ui_demo1.vsh b/examples/web/ui_demo1.vsh new file mode 100755 index 00000000..5ee7cc0f --- /dev/null +++ b/examples/web/ui_demo1.vsh @@ -0,0 +1,13 @@ +#!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run + +import freeflowuniverse.herolib.web.ui + +fn main() { + println('Starting UI test server on port 8080...') + println('Visit http://localhost:8080 to see the admin interface') + + ui.start( + title: 'Test Admin Panel' + port: 8080 + )! +} \ No newline at end of file diff --git a/lib/web/ui/factory.v b/lib/web/ui/factory.v new file mode 100644 index 00000000..d4a93dd7 --- /dev/null +++ b/lib/web/ui/factory.v @@ -0,0 +1,283 @@ +module ui + +import veb +import os + +// Public Context type for veb +pub struct Context { + veb.Context +} + +// Simple tree menu structure +pub struct MenuItem { +pub: + title string + href string + children []MenuItem +} + +// Factory args +@[params] +pub struct FactoryArgs { +pub mut: + name string = 'default' + port int = 8080 + title string = 'Admin' + menu []MenuItem +} + +// The App holds server state and config +pub struct App { + veb.StaticHandler +pub mut: + title string + menu []MenuItem + port int +} + +// Global registry (multi-instance support by name) +__global ( + uireg map[string]&App +) + +// Create a new app (does not start the server) +pub fn new(args FactoryArgs) !&App { + name := if args.name.len == 0 { 'default' } else { args.name } + if app := uireg[name] { + return app + } + mut app := &App{ + title: args.title + menu: if args.menu.len > 0 { args.menu } else { default_menu() } + port: args.port + } + uireg[name] = app + return app +} + +// Get a named app +pub fn get(name string) !&App { + mut app := uireg[name] or { + return error('ui: app "${name}" not found, call ui.new(...) first') + } + return app +} + +// Get default app (creates if not existing) +pub fn default() !&App { + if uireg.len == 0 { + return new(port: 8080)! + } + return get('default')! +} + +// Start the webserver (blocking) +pub fn start(args FactoryArgs) ! { + mut app := new(args)! + veb.run[App, Context](mut app, app.port) +} + +// Routes + +// Redirect root to /admin +@[get; '/'] +pub fn (app &App) root(mut ctx Context) veb.Result { + return ctx.redirect('/admin') +} + +// Admin home page +@[get; '/admin'] +pub fn (app &App) admin_index(mut ctx Context) veb.Result { + return ctx.html(app.render_admin('/', 'Welcome')) +} + +// Catch-all content under /admin/* +@[get; '/admin/:path...'] +pub fn (app &App) admin_section(mut ctx Context, path string) veb.Result { + // Render current path in the main content + return ctx.html(app.render_admin(path, 'Content')) +} + +// View rendering using external template + +fn (app &App) render_admin(path string, heading string) string { + // Get the template file path relative to the module + template_path := os.join_path(os.dir(@FILE), 'templates', 'admin_layout.html') + + // Read the template file + template_content := os.read_file(template_path) or { + // Fallback to inline template if file not found + return app.render_admin_fallback(path, heading) + } + + // Generate menu HTML + menu_content := menu_html(app.menu, 0, 'm') + + // Simple template variable replacement + mut result := template_content + result = result.replace('{{.title}}', app.title) + result = result.replace('{{.heading}}', heading) + result = result.replace('{{.path}}', path) + result = result.replace('{{.menu_html}}', menu_content) + + return result +} + +// Fallback rendering method (inline template) +fn (app &App) render_admin_fallback(path string, heading string) string { + return ' + + + + + + ${app.title} + + + + + + + + +
+
+
+
${heading}
+ /admin/${path} +
+
+
+

This is a placeholder admin content area for: /admin/${path}.

+

Use the treeview on the left to navigate.

+
+
+
+
+ + + + +' +} + +// Recursive menu renderer + +fn menu_html(items []MenuItem, depth int, prefix string) string { + mut out := []string{} + for i, it in items { + id := '${prefix}_${depth}_${i}' + if it.children.len > 0 { + // expandable group + out << '
' + out << '' + out << '${it.title}' + out << '' + out << '
' + out << '
' + out << menu_html(it.children, depth + 1, id) + out << '
' + out << '
' + out << '
' + } else { + // leaf + out << '' + } + } + return out.join('\n') +} + +// Default sample menu +fn default_menu() []MenuItem { + return [ + MenuItem{ + title: 'Dashboard' + href: '/admin' + }, + MenuItem{ + title: 'Users' + children: [ + MenuItem{ title: 'Overview', href: '/admin/users/overview' }, + MenuItem{ title: 'Create', href: '/admin/users/create' }, + MenuItem{ title: 'Roles', href: '/admin/users/roles' }, + ] + }, + MenuItem{ + title: 'Content' + children: [ + MenuItem{ title: 'Pages', href: '/admin/content/pages' }, + MenuItem{ title: 'Media', href: '/admin/content/media' }, + MenuItem{ + title: 'Settings' + children: [ + MenuItem{ title: 'SEO', href: '/admin/content/settings/seo' }, + MenuItem{ title: 'Themes', href: '/admin/content/settings/themes' }, + ] + }, + ] + }, + MenuItem{ + title: 'System' + children: [ + MenuItem{ title: 'Status', href: '/admin/system/status' }, + MenuItem{ title: 'Logs', href: '/admin/system/logs' }, + MenuItem{ title: 'Backups', href: '/admin/system/backups' }, + ] + }, + ] +} \ No newline at end of file diff --git a/lib/web/ui/templates/admin_layout.html b/lib/web/ui/templates/admin_layout.html new file mode 100644 index 00000000..778e61ea --- /dev/null +++ b/lib/web/ui/templates/admin_layout.html @@ -0,0 +1,87 @@ + + + + + + {{.title}} + + + + + + + + +
+
+
+
{{.heading}}
+ /admin/{{.path}} +
+
+
+

This is a placeholder admin content area for: /admin/{{.path}}.

+

Use the treeview on the left to navigate.

+
+
+
+
+ + + + \ No newline at end of file