feat(admin): runtime config builders with local save/publish

This commit is contained in:
Ashwin Kumar 2026-03-16 23:20:54 +01:00
parent 4adaec262c
commit e5ce1ef1b3
31 changed files with 9892 additions and 2 deletions

26
.gitignore vendored
View file

@ -1,6 +1,28 @@
node_modules
dist
.wrangler
.output
.vercel
.netlify
.vinxi
app.config.timestamp_*.js
# Environment
.env
.env.*
.env*.local
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
*.launch
.settings/
# Temp
gitignore
# System Files
.DS_Store
Thumbs.db

14
.nitro/types/nitro-config.d.ts vendored Normal file
View file

@ -0,0 +1,14 @@
// Generated by nitro
// App Config
import type { Defu } from 'defu'
type UserAppConfig = Defu<{}, []>
declare module "nitropack/types" {
interface AppConfig extends UserAppConfig {}
}
export {}

136
.nitro/types/nitro-imports.d.ts vendored Normal file
View file

@ -0,0 +1,136 @@
declare global {
const appendCorsHeaders: typeof import('../../node_modules/h3').appendCorsHeaders
const appendCorsPreflightHeaders: typeof import('../../node_modules/h3').appendCorsPreflightHeaders
const appendHeader: typeof import('../../node_modules/h3').appendHeader
const appendHeaders: typeof import('../../node_modules/h3').appendHeaders
const appendResponseHeader: typeof import('../../node_modules/h3').appendResponseHeader
const appendResponseHeaders: typeof import('../../node_modules/h3').appendResponseHeaders
const assertMethod: typeof import('../../node_modules/h3').assertMethod
const cachedEventHandler: typeof import('../../node_modules/nitropack/dist/runtime/internal/cache').cachedEventHandler
const cachedFunction: typeof import('../../node_modules/nitropack/dist/runtime/internal/cache').cachedFunction
const callNodeListener: typeof import('../../node_modules/h3').callNodeListener
const clearResponseHeaders: typeof import('../../node_modules/h3').clearResponseHeaders
const clearSession: typeof import('../../node_modules/h3').clearSession
const createApp: typeof import('../../node_modules/h3').createApp
const createAppEventHandler: typeof import('../../node_modules/h3').createAppEventHandler
const createError: typeof import('../../node_modules/h3').createError
const createEvent: typeof import('../../node_modules/h3').createEvent
const createEventStream: typeof import('../../node_modules/h3').createEventStream
const createRouter: typeof import('../../node_modules/h3').createRouter
const defaultContentType: typeof import('../../node_modules/h3').defaultContentType
const defineCachedEventHandler: typeof import('../../node_modules/nitropack/dist/runtime/internal/cache').defineCachedEventHandler
const defineCachedFunction: typeof import('../../node_modules/nitropack/dist/runtime/internal/cache').defineCachedFunction
const defineEventHandler: typeof import('../../node_modules/h3').defineEventHandler
const defineLazyEventHandler: typeof import('../../node_modules/h3').defineLazyEventHandler
const defineNitroErrorHandler: typeof import('../../node_modules/nitropack/dist/runtime/internal/error/utils').defineNitroErrorHandler
const defineNitroPlugin: typeof import('../../node_modules/nitropack/dist/runtime/internal/plugin').defineNitroPlugin
const defineNodeListener: typeof import('../../node_modules/h3').defineNodeListener
const defineNodeMiddleware: typeof import('../../node_modules/h3').defineNodeMiddleware
const defineRenderHandler: typeof import('../../node_modules/nitropack/dist/runtime/internal/renderer').defineRenderHandler
const defineRequestMiddleware: typeof import('../../node_modules/h3').defineRequestMiddleware
const defineResponseMiddleware: typeof import('../../node_modules/h3').defineResponseMiddleware
const defineRouteMeta: typeof import('../../node_modules/nitropack/dist/runtime/internal/meta').defineRouteMeta
const defineTask: typeof import('../../node_modules/nitropack/dist/runtime/internal/task').defineTask
const defineWebSocket: typeof import('../../node_modules/h3').defineWebSocket
const defineWebSocketHandler: typeof import('../../node_modules/h3').defineWebSocketHandler
const deleteCookie: typeof import('../../node_modules/h3').deleteCookie
const dynamicEventHandler: typeof import('../../node_modules/h3').dynamicEventHandler
const eventHandler: typeof import('../../node_modules/h3').eventHandler
const fetchWithEvent: typeof import('../../node_modules/h3').fetchWithEvent
const fromNodeMiddleware: typeof import('../../node_modules/h3').fromNodeMiddleware
const fromPlainHandler: typeof import('../../node_modules/h3').fromPlainHandler
const fromWebHandler: typeof import('../../node_modules/h3').fromWebHandler
const getCookie: typeof import('../../node_modules/h3').getCookie
const getHeader: typeof import('../../node_modules/h3').getHeader
const getHeaders: typeof import('../../node_modules/h3').getHeaders
const getMethod: typeof import('../../node_modules/h3').getMethod
const getProxyRequestHeaders: typeof import('../../node_modules/h3').getProxyRequestHeaders
const getQuery: typeof import('../../node_modules/h3').getQuery
const getRequestFingerprint: typeof import('../../node_modules/h3').getRequestFingerprint
const getRequestHeader: typeof import('../../node_modules/h3').getRequestHeader
const getRequestHeaders: typeof import('../../node_modules/h3').getRequestHeaders
const getRequestHost: typeof import('../../node_modules/h3').getRequestHost
const getRequestIP: typeof import('../../node_modules/h3').getRequestIP
const getRequestPath: typeof import('../../node_modules/h3').getRequestPath
const getRequestProtocol: typeof import('../../node_modules/h3').getRequestProtocol
const getRequestURL: typeof import('../../node_modules/h3').getRequestURL
const getRequestWebStream: typeof import('../../node_modules/h3').getRequestWebStream
const getResponseHeader: typeof import('../../node_modules/h3').getResponseHeader
const getResponseHeaders: typeof import('../../node_modules/h3').getResponseHeaders
const getResponseStatus: typeof import('../../node_modules/h3').getResponseStatus
const getResponseStatusText: typeof import('../../node_modules/h3').getResponseStatusText
const getRouteRules: typeof import('../../node_modules/nitropack/dist/runtime/internal/route-rules').getRouteRules
const getRouterParam: typeof import('../../node_modules/h3').getRouterParam
const getRouterParams: typeof import('../../node_modules/h3').getRouterParams
const getSession: typeof import('../../node_modules/h3').getSession
const getValidatedQuery: typeof import('../../node_modules/h3').getValidatedQuery
const getValidatedRouterParams: typeof import('../../node_modules/h3').getValidatedRouterParams
const handleCacheHeaders: typeof import('../../node_modules/h3').handleCacheHeaders
const handleCors: typeof import('../../node_modules/h3').handleCors
const isCorsOriginAllowed: typeof import('../../node_modules/h3').isCorsOriginAllowed
const isError: typeof import('../../node_modules/h3').isError
const isEvent: typeof import('../../node_modules/h3').isEvent
const isEventHandler: typeof import('../../node_modules/h3').isEventHandler
const isMethod: typeof import('../../node_modules/h3').isMethod
const isPreflightRequest: typeof import('../../node_modules/h3').isPreflightRequest
const isStream: typeof import('../../node_modules/h3').isStream
const isWebResponse: typeof import('../../node_modules/h3').isWebResponse
const lazyEventHandler: typeof import('../../node_modules/h3').lazyEventHandler
const nitroPlugin: typeof import('../../node_modules/nitropack/dist/runtime/internal/plugin').nitroPlugin
const parseCookies: typeof import('../../node_modules/h3').parseCookies
const promisifyNodeListener: typeof import('../../node_modules/h3').promisifyNodeListener
const proxyRequest: typeof import('../../node_modules/h3').proxyRequest
const readBody: typeof import('../../node_modules/h3').readBody
const readFormData: typeof import('../../node_modules/h3').readFormData
const readMultipartFormData: typeof import('../../node_modules/h3').readMultipartFormData
const readRawBody: typeof import('../../node_modules/h3').readRawBody
const readValidatedBody: typeof import('../../node_modules/h3').readValidatedBody
const removeResponseHeader: typeof import('../../node_modules/h3').removeResponseHeader
const runTask: typeof import('../../node_modules/nitropack/dist/runtime/internal/task').runTask
const sanitizeStatusCode: typeof import('../../node_modules/h3').sanitizeStatusCode
const sanitizeStatusMessage: typeof import('../../node_modules/h3').sanitizeStatusMessage
const sealSession: typeof import('../../node_modules/h3').sealSession
const send: typeof import('../../node_modules/h3').send
const sendError: typeof import('../../node_modules/h3').sendError
const sendIterable: typeof import('../../node_modules/h3').sendIterable
const sendNoContent: typeof import('../../node_modules/h3').sendNoContent
const sendProxy: typeof import('../../node_modules/h3').sendProxy
const sendRedirect: typeof import('../../node_modules/h3').sendRedirect
const sendStream: typeof import('../../node_modules/h3').sendStream
const sendWebResponse: typeof import('../../node_modules/h3').sendWebResponse
const serveStatic: typeof import('../../node_modules/h3').serveStatic
const setCookie: typeof import('../../node_modules/h3').setCookie
const setHeader: typeof import('../../node_modules/h3').setHeader
const setHeaders: typeof import('../../node_modules/h3').setHeaders
const setResponseHeader: typeof import('../../node_modules/h3').setResponseHeader
const setResponseHeaders: typeof import('../../node_modules/h3').setResponseHeaders
const setResponseStatus: typeof import('../../node_modules/h3').setResponseStatus
const splitCookiesString: typeof import('../../node_modules/h3').splitCookiesString
const toEventHandler: typeof import('../../node_modules/h3').toEventHandler
const toNodeListener: typeof import('../../node_modules/h3').toNodeListener
const toPlainHandler: typeof import('../../node_modules/h3').toPlainHandler
const toWebHandler: typeof import('../../node_modules/h3').toWebHandler
const toWebRequest: typeof import('../../node_modules/h3').toWebRequest
const unsealSession: typeof import('../../node_modules/h3').unsealSession
const updateSession: typeof import('../../node_modules/h3').updateSession
const useAppConfig: typeof import('../../node_modules/nitropack/dist/runtime/internal/config').useAppConfig
const useBase: typeof import('../../node_modules/h3').useBase
const useEvent: typeof import('../../node_modules/nitropack/dist/runtime/internal/context').useEvent
const useNitroApp: typeof import('../../node_modules/nitropack/dist/runtime/internal/app').useNitroApp
const useRuntimeConfig: typeof import('../../node_modules/nitropack/dist/runtime/internal/config').useRuntimeConfig
const useSession: typeof import('../../node_modules/h3').useSession
const useStorage: typeof import('../../node_modules/nitropack/dist/runtime/internal/storage').useStorage
const writeEarlyHints: typeof import('../../node_modules/h3').writeEarlyHints
}
export { useNitroApp } from 'nitropack/runtime/internal/app';
export { useRuntimeConfig, useAppConfig } from 'nitropack/runtime/internal/config';
export { defineNitroPlugin, nitroPlugin } from 'nitropack/runtime/internal/plugin';
export { defineCachedFunction, defineCachedEventHandler, cachedFunction, cachedEventHandler } from 'nitropack/runtime/internal/cache';
export { useStorage } from 'nitropack/runtime/internal/storage';
export { defineRenderHandler } from 'nitropack/runtime/internal/renderer';
export { defineRouteMeta } from 'nitropack/runtime/internal/meta';
export { getRouteRules } from 'nitropack/runtime/internal/route-rules';
export { useEvent } from 'nitropack/runtime/internal/context';
export { defineTask, runTask } from 'nitropack/runtime/internal/task';
export { defineNitroErrorHandler } from 'nitropack/runtime/internal/error/utils';
export { appendCorsHeaders, appendCorsPreflightHeaders, appendHeader, appendHeaders, appendResponseHeader, appendResponseHeaders, assertMethod, callNodeListener, clearResponseHeaders, clearSession, createApp, createAppEventHandler, createError, createEvent, createEventStream, createRouter, defaultContentType, defineEventHandler, defineLazyEventHandler, defineNodeListener, defineNodeMiddleware, defineRequestMiddleware, defineResponseMiddleware, defineWebSocket, defineWebSocketHandler, deleteCookie, dynamicEventHandler, eventHandler, fetchWithEvent, fromNodeMiddleware, fromPlainHandler, fromWebHandler, getCookie, getHeader, getHeaders, getMethod, getProxyRequestHeaders, getQuery, getRequestFingerprint, getRequestHeader, getRequestHeaders, getRequestHost, getRequestIP, getRequestPath, getRequestProtocol, getRequestURL, getRequestWebStream, getResponseHeader, getResponseHeaders, getResponseStatus, getResponseStatusText, getRouterParam, getRouterParams, getSession, getValidatedQuery, getValidatedRouterParams, handleCacheHeaders, handleCors, isCorsOriginAllowed, isError, isEvent, isEventHandler, isMethod, isPreflightRequest, isStream, isWebResponse, lazyEventHandler, parseCookies, promisifyNodeListener, proxyRequest, readBody, readFormData, readMultipartFormData, readRawBody, readValidatedBody, removeResponseHeader, sanitizeStatusCode, sanitizeStatusMessage, sealSession, send, sendError, sendIterable, sendNoContent, sendProxy, sendRedirect, sendStream, sendWebResponse, serveStatic, setCookie, setHeader, setHeaders, setResponseHeader, setResponseHeaders, setResponseStatus, splitCookiesString, toEventHandler, toNodeListener, toPlainHandler, toWebHandler, toWebRequest, unsealSession, updateSession, useBase, useSession, writeEarlyHints } from 'h3';

8
.nitro/types/nitro-routes.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
// Generated by nitro
import type { Serialize, Simplify } from "nitropack/types";
declare module "nitropack/types" {
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T
interface InternalApi {
}
}
export {}

3
.nitro/types/nitro.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
/// <reference path="./nitro-routes.d.ts" />
/// <reference path="./nitro-config.d.ts" />
/// <reference path="./nitro-imports.d.ts" />

View file

@ -6,3 +6,5 @@ SolidStart migration target for `nxtgauge-nov-2025-frontend` (admin panel).
Port admin modules one by one with strict API/permission parity.
See `docs/MIGRATION_MASTER_PLAN.md`.
## This project was created with the [Solid CLI](https://github.com/solidjs-community/solid-cli)

8981
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

19
package.json Normal file
View file

@ -0,0 +1,19 @@
{
"name": "nxtgauge-admin-solid",
"type": "module",
"scripts": {
"dev": "vinxi dev",
"build": "vinxi build",
"start": "vinxi start"
},
"dependencies": {
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.0",
"@solidjs/start": "^1.3.2",
"solid-js": "^1.9.5",
"vinxi": "^0.5.7"
},
"engines": {
"node": ">=20"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

BIN
public/nxtgauge-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
public/nxtgauge-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

188
src/app.css Normal file
View file

@ -0,0 +1,188 @@
@import url('https://fonts.googleapis.com/css2?family=Exo+2:wght@400;500;600;700;800&display=swap');
:root {
--brand-orange: #fd6216;
--brand-navy: #050026;
--brand-orange-50: #fff1e8;
--brand-orange-100: #ffe2d2;
--brand-orange-200: #ffc9ac;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: #f3f4f8;
font-family: 'Exo 2', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: var(--brand-navy);
}
.shell {
display: grid;
grid-template-columns: 280px 1fr;
min-height: 100vh;
}
.sidebar {
border-right: 1px solid #dbe1ec;
background: #fcfcfd;
padding: 16px;
}
.brand {
margin: 4px 0 18px;
}
.brand img {
height: 42px;
width: auto;
object-fit: contain;
}
.nav-item {
display: block;
text-decoration: none;
color: #475569;
border: 1px solid transparent;
background: #fff;
border-radius: 12px;
padding: 10px 12px;
margin-bottom: 8px;
font-size: 14px;
}
.nav-item:hover {
border-color: #dbe1ec;
color: #1f2937;
}
.nav-item.active {
border-color: var(--brand-orange-200);
background: linear-gradient(90deg, var(--brand-orange-50), var(--brand-orange-100));
color: #111827;
font-weight: 600;
}
.main {
padding: 24px;
}
.page-title {
margin: 0;
font-size: 28px;
font-weight: 700;
color: #0f172a;
}
.page-subtitle {
margin-top: 8px;
color: #64748b;
}
.grid {
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: 16px;
margin-top: 20px;
}
.card {
background: #fff;
border: 1px solid #dbe1ec;
border-radius: 16px;
padding: 16px;
}
.card h2 {
margin: 0 0 12px;
font-size: 18px;
}
.field {
margin-bottom: 12px;
}
.field label {
display: block;
margin-bottom: 6px;
font-size: 13px;
font-weight: 600;
color: #334155;
}
.field input,
.field textarea,
.field select {
width: 100%;
border: 1px solid #cbd5e1;
border-radius: 10px;
padding: 10px;
font-size: 14px;
background: #fff;
}
.actions {
display: flex;
gap: 10px;
margin-top: 16px;
}
.btn {
border: 1px solid #cbd5e1;
background: #fff;
color: #334155;
border-radius: 10px;
padding: 10px 14px;
font-weight: 600;
cursor: pointer;
}
.btn.primary {
border-color: var(--brand-orange);
background: var(--brand-orange);
color: #fff;
}
.json {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
line-height: 1.5;
background: #0f172a;
color: #e2e8f0;
border-radius: 12px;
padding: 12px;
overflow: auto;
max-height: 560px;
}
.notice {
margin-top: 8px;
font-size: 12px;
color: #64748b;
}
@media (max-width: 1000px) {
.shell {
grid-template-columns: 1fr;
}
.sidebar {
border-right: 0;
border-bottom: 1px solid #dbe1ec;
}
.grid {
grid-template-columns: 1fr;
}
}
.inline-note {
margin-top: 10px;
font-size: 12px;
color: #0f766e;
font-weight: 600;
}

20
src/app.tsx Normal file
View file

@ -0,0 +1,20 @@
import { MetaProvider, Title } from "@solidjs/meta";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
import "./app.css";
export default function App() {
return (
<Router
root={props => (
<MetaProvider>
<Title>ADMIN PANEL | NXTGAUGE</Title>
<Suspense>{props.children}</Suspense>
</MetaProvider>
)}
>
<FileRoutes />
</Router>
);
}

View file

@ -0,0 +1,11 @@
import type { JSX } from 'solid-js';
import AdminSidebar from './AdminSidebar';
export default function AdminShell(props: { children: JSX.Element }) {
return (
<div class="shell">
<AdminSidebar />
<main class="main">{props.children}</main>
</div>
);
}

View file

@ -0,0 +1,25 @@
import { A, useLocation } from '@solidjs/router';
const links = [
{ href: '/admin/runtime-roles/new', label: 'Create Role' },
{ href: '/admin/role-ui-configs/new', label: 'Create Dashboard' },
{ href: '/admin/onboarding-schemas/new', label: 'Create Onboarding Flow' },
];
export default function AdminSidebar() {
const location = useLocation();
return (
<aside class="sidebar">
<div class="brand">
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" />
</div>
{links.map((item) => (
<A href={item.href} class={`nav-item ${location.pathname === item.href ? 'active' : ''}`}>
{item.label}
</A>
))}
<p class="notice">Same UI flow, simpler builder experience.</p>
</aside>
);
}

View file

@ -0,0 +1,21 @@
.increment {
font-family: inherit;
font-size: inherit;
padding: 1em 2em;
color: #335d92;
background-color: rgba(68, 107, 158, 0.1);
border-radius: 2em;
border: 2px solid rgba(68, 107, 158, 0);
outline: none;
width: 200px;
font-variant-numeric: tabular-nums;
cursor: pointer;
}
.increment:focus {
border: 2px solid #335d92;
}
.increment:active {
background-color: rgba(68, 107, 158, 0.2);
}

View file

@ -0,0 +1,11 @@
import { createSignal } from "solid-js";
import "./Counter.css";
export default function Counter() {
const [count, setCount] = createSignal(0);
return (
<button class="increment" onClick={() => setCount(count() + 1)} type="button">
Clicks: {count()}
</button>
);
}

4
src/entry-client.tsx Normal file
View file

@ -0,0 +1,4 @@
// @refresh reload
import { mount, StartClient } from "@solidjs/start/client";
mount(() => <StartClient />, document.getElementById("app")!);

21
src/entry-server.tsx Normal file
View file

@ -0,0 +1,21 @@
// @refresh reload
import { createHandler, StartServer } from "@solidjs/start/server";
export default createHandler(() => (
<StartServer
document={({ assets, children, scripts }) => (
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
{assets}
</head>
<body>
<div id="app">{children}</div>
{scripts}
</body>
</html>
)}
/>
));

1
src/global.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="@solidjs/start/env" />

View file

@ -0,0 +1,45 @@
export type RuntimeRecordType = 'role' | 'dashboard' | 'onboarding';
export type RuntimeRecordStatus = 'draft' | 'published';
const KEY = 'nxtgauge_admin_runtime_builder_v1';
type RuntimeStore = {
role: Record<string, unknown>;
dashboard: Record<string, unknown>;
onboarding: Record<string, unknown>;
};
function readStore(): RuntimeStore {
if (typeof window === 'undefined') {
return { role: {}, dashboard: {}, onboarding: {} };
}
const raw = window.localStorage.getItem(KEY);
if (!raw) return { role: {}, dashboard: {}, onboarding: {} };
try {
const parsed = JSON.parse(raw) as Partial<RuntimeStore>;
return {
role: parsed.role || {},
dashboard: parsed.dashboard || {},
onboarding: parsed.onboarding || {},
};
} catch {
return { role: {}, dashboard: {}, onboarding: {} };
}
}
function writeStore(store: RuntimeStore) {
if (typeof window === 'undefined') return;
window.localStorage.setItem(KEY, JSON.stringify(store));
}
export function saveRuntimeConfig(type: RuntimeRecordType, key: string, payload: unknown, status: RuntimeRecordStatus) {
const store = readStore();
const bucket = store[type];
bucket[key] = {
status,
updatedAt: new Date().toISOString(),
payload,
};
writeStore(store);
}

25
src/lib/runtime/types.ts Normal file
View file

@ -0,0 +1,25 @@
export type RuntimeRoleConfig = {
roleKey: string;
displayName: string;
vertical: 'jobs' | 'marketplace';
onboardingSchemaId: string;
enabledModules: string[];
requiresOnboardingApproval: boolean;
};
export type RuntimeDashboardConfig = {
roleKey: string;
sidebar: Array<{ key: string; label: string; route: string }>;
widgets: Array<{ key: string; title: string; enabled: boolean }>;
};
export type RuntimeOnboardingConfig = {
schemaId: string;
roleKey: string;
version: number;
steps: Array<{
id: string;
title: string;
fields: Array<{ id: string; label: string; type: string; required?: boolean }>;
}>;
};

19
src/routes/[...404].tsx Normal file
View file

@ -0,0 +1,19 @@
import { Title } from "@solidjs/meta";
import { HttpStatusCode } from "@solidjs/start";
export default function NotFound() {
return (
<main>
<Title>Not Found</Title>
<HttpStatusCode code={404} />
<h1>Page Not Found</h1>
<p>
Visit{" "}
<a href="https://start.solidjs.com" target="_blank">
start.solidjs.com
</a>{" "}
to learn how to build SolidStart apps.
</p>
</main>
);
}

10
src/routes/about.tsx Normal file
View file

@ -0,0 +1,10 @@
import { Title } from "@solidjs/meta";
export default function About() {
return (
<main>
<Title>About</Title>
<h1>About</h1>
</main>
);
}

View file

@ -0,0 +1,11 @@
import { redirect } from '@solidjs/router';
export const route = {
preload() {
throw redirect('/admin/runtime-roles/new');
},
};
export default function AdminIndex() {
return null;
}

View file

@ -0,0 +1,87 @@
import { createSignal } from 'solid-js';
import AdminShell from '~/components/AdminShell';
import { saveRuntimeConfig } from '~/lib/runtime/storage';
import type { RuntimeOnboardingConfig } from '~/lib/runtime/types';
export default function CreateOnboardingSchemaPage() {
const [config, setConfig] = createSignal<RuntimeOnboardingConfig>({
schemaId: 'photographer_onboarding_v1',
roleKey: 'PHOTOGRAPHER',
version: 1,
steps: [
{
id: 'profile',
title: 'Profile Details',
fields: [
{ id: 'full_name', label: 'Full Name', type: 'text', required: true },
{ id: 'experience', label: 'Experience', type: 'number', required: true },
],
},
],
});
const [statusMessage, setStatusMessage] = createSignal('');
const persist = (status: 'draft' | 'published') => {
const payload = config();
if (!payload.schemaId.trim()) {
setStatusMessage('Schema ID is required before saving.');
return;
}
saveRuntimeConfig('onboarding', payload.schemaId, payload, status);
setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Onboarding schema published in runtime storage.');
};
return (
<AdminShell>
<h1 class="page-title">Create Onboarding Flow</h1>
<p class="page-subtitle">Build onboarding in the same place with a simple step editor and visible runtime JSON.</p>
<div class="grid">
<section class="card">
<h2>Onboarding Builder</h2>
<div class="field">
<label>Schema ID</label>
<input value={config().schemaId} onInput={(e) => setConfig({ ...config(), schemaId: e.currentTarget.value })} />
</div>
<div class="field">
<label>Role Key</label>
<input value={config().roleKey} onInput={(e) => setConfig({ ...config(), roleKey: e.currentTarget.value.toUpperCase() })} />
</div>
<div class="field">
<label>Version</label>
<input type="number" value={config().version} onInput={(e) => setConfig({ ...config(), version: Number(e.currentTarget.value || 1) })} />
</div>
<div class="field">
<label>Steps (one title per line, quick mode)</label>
<textarea
rows={8}
value={config().steps.map((s) => s.title).join('\n')}
onInput={(e) =>
setConfig({
...config(),
steps: e.currentTarget.value
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.map((title, index) => ({
id: `step_${index + 1}`,
title,
fields: [{ id: `field_${index + 1}`, label: 'Sample Field', type: 'text', required: true }],
})),
})
}
/>
</div>
<div class="actions">
<button class="btn" onClick={() => persist('draft')}>Save Draft</button>
<button class="btn primary" onClick={() => persist('published')}>Publish</button>
</div>
{statusMessage() && <p class="inline-note">{statusMessage()}</p>}
</section>
<section class="card">
<h2>Runtime Config Preview</h2>
<pre class="json">{JSON.stringify(config(), null, 2)}</pre>
</section>
</div>
</AdminShell>
);
}

View file

@ -0,0 +1,93 @@
import { createSignal } from 'solid-js';
import AdminShell from '~/components/AdminShell';
import { saveRuntimeConfig } from '~/lib/runtime/storage';
import type { RuntimeDashboardConfig } from '~/lib/runtime/types';
export default function CreateDashboardPage() {
const [config, setConfig] = createSignal<RuntimeDashboardConfig>({
roleKey: 'PHOTOGRAPHER',
sidebar: [
{ key: 'dashboard', label: 'Dashboard', route: '/workspace/dashboard' },
{ key: 'leads', label: 'Leads', route: '/workspace/leads' },
{ key: 'portfolio', label: 'Portfolio', route: '/workspace/portfolio' },
{ key: 'verification', label: 'Verification', route: '/workspace/verification' },
],
widgets: [
{ key: 'active-leads', title: 'Active Leads', enabled: true },
{ key: 'recent-requests', title: 'Recent Requests', enabled: true },
],
});
const [statusMessage, setStatusMessage] = createSignal('');
const persist = (status: 'draft' | 'published') => {
const payload = config();
if (!payload.roleKey.trim()) {
setStatusMessage('Role key is required before saving.');
return;
}
saveRuntimeConfig('dashboard', payload.roleKey, payload, status);
setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Dashboard config published in runtime storage.');
};
return (
<AdminShell>
<h1 class="page-title">Create Dashboard</h1>
<p class="page-subtitle">Edit sidebar and widgets directly. Runtime config stays visible while building.</p>
<div class="grid">
<section class="card">
<h2>Dashboard Builder</h2>
<div class="field">
<label>Role Key</label>
<input value={config().roleKey} onInput={(e) => setConfig({ ...config(), roleKey: e.currentTarget.value.toUpperCase() })} />
</div>
<div class="field">
<label>Sidebar Items (label|route per line)</label>
<textarea
rows={8}
value={config().sidebar.map((x) => `${x.label}|${x.route}`).join('\n')}
onInput={(e) =>
setConfig({
...config(),
sidebar: e.currentTarget.value
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const [label, route] = line.split('|').map((part) => part.trim());
return { key: label.toLowerCase().replace(/\s+/g, '-'), label, route };
}),
})
}
/>
</div>
<div class="field">
<label>Widgets (title per line)</label>
<textarea
rows={6}
value={config().widgets.map((x) => x.title).join('\n')}
onInput={(e) =>
setConfig({
...config(),
widgets: e.currentTarget.value
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.map((title) => ({ key: title.toLowerCase().replace(/\s+/g, '-'), title, enabled: true })),
})
}
/>
</div>
<div class="actions">
<button class="btn" onClick={() => persist('draft')}>Save Draft</button>
<button class="btn primary" onClick={() => persist('published')}>Publish</button>
</div>
{statusMessage() && <p class="inline-note">{statusMessage()}</p>}
</section>
<section class="card">
<h2>Runtime Config Preview</h2>
<pre class="json">{JSON.stringify(config(), null, 2)}</pre>
</section>
</div>
</AdminShell>
);
}

View file

@ -0,0 +1,80 @@
import { createSignal } from 'solid-js';
import AdminShell from '~/components/AdminShell';
import { saveRuntimeConfig } from '~/lib/runtime/storage';
import type { RuntimeRoleConfig } from '~/lib/runtime/types';
export default function CreateRolePage() {
const [config, setConfig] = createSignal<RuntimeRoleConfig>({
roleKey: 'PHOTOGRAPHER',
displayName: 'Photographer',
vertical: 'marketplace',
onboardingSchemaId: 'photographer_onboarding_v1',
enabledModules: ['profile', 'leads', 'portfolio', 'verification', 'notifications'],
requiresOnboardingApproval: true,
});
const [statusMessage, setStatusMessage] = createSignal('');
const persist = (status: 'draft' | 'published') => {
const payload = config();
if (!payload.roleKey.trim()) {
setStatusMessage('Role key is required before saving.');
return;
}
saveRuntimeConfig('role', payload.roleKey, payload, status);
setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Role config published in runtime storage.');
};
return (
<AdminShell>
<h1 class="page-title">Create Role</h1>
<p class="page-subtitle">Use the same admin flow, with simpler fields and live runtime config visibility.</p>
<div class="grid">
<section class="card">
<h2>Role Builder</h2>
<div class="field">
<label>Role Key</label>
<input value={config().roleKey} onInput={(e) => setConfig({ ...config(), roleKey: e.currentTarget.value.toUpperCase() })} />
</div>
<div class="field">
<label>Display Name</label>
<input value={config().displayName} onInput={(e) => setConfig({ ...config(), displayName: e.currentTarget.value })} />
</div>
<div class="field">
<label>Vertical</label>
<select value={config().vertical} onInput={(e) => setConfig({ ...config(), vertical: e.currentTarget.value as 'jobs' | 'marketplace' })}>
<option value="marketplace">Marketplace</option>
<option value="jobs">Jobs</option>
</select>
</div>
<div class="field">
<label>Onboarding Schema ID</label>
<input value={config().onboardingSchemaId} onInput={(e) => setConfig({ ...config(), onboardingSchemaId: e.currentTarget.value })} />
</div>
<div class="field">
<label>Enabled Modules (comma separated)</label>
<input value={config().enabledModules.join(', ')} onInput={(e) => setConfig({ ...config(), enabledModules: e.currentTarget.value.split(',').map((x) => x.trim()).filter(Boolean) })} />
</div>
<div class="field">
<label>
<input
type="checkbox"
checked={config().requiresOnboardingApproval}
onInput={(e) => setConfig({ ...config(), requiresOnboardingApproval: e.currentTarget.checked })}
/>
{' '}Requires onboarding approval
</label>
</div>
<div class="actions">
<button class="btn" onClick={() => persist('draft')}>Save Draft</button>
<button class="btn primary" onClick={() => persist('published')}>Publish</button>
</div>
{statusMessage() && <p class="inline-note">{statusMessage()}</p>}
</section>
<section class="card">
<h2>Runtime Config Preview</h2>
<pre class="json">{JSON.stringify(config(), null, 2)}</pre>
</section>
</div>
</AdminShell>
);
}

11
src/routes/index.tsx Normal file
View file

@ -0,0 +1,11 @@
import { redirect } from '@solidjs/router';
export const route = {
preload() {
throw redirect('/admin/runtime-roles/new');
},
};
export default function Home() {
return null;
}

19
tsconfig.json Normal file
View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"allowJs": true,
"strict": true,
"noEmit": true,
"types": ["vite/client"],
"isolatedModules": true,
"paths": {
"~/*": ["./src/*"]
}
}
}

3
vite.config.ts Normal file
View file

@ -0,0 +1,3 @@
import { defineConfig } from '@solidjs/start/config';
export default defineConfig({});