feat(admin): runtime config builders with local save/publish
This commit is contained in:
parent
4adaec262c
commit
e5ce1ef1b3
31 changed files with 9892 additions and 2 deletions
26
.gitignore
vendored
26
.gitignore
vendored
|
|
@ -1,6 +1,28 @@
|
||||||
node_modules
|
|
||||||
dist
|
dist
|
||||||
|
.wrangler
|
||||||
.output
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.vinxi
|
||||||
|
app.config.timestamp_*.js
|
||||||
|
|
||||||
|
# Environment
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env*.local
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
|
||||||
|
# Temp
|
||||||
|
gitignore
|
||||||
|
|
||||||
|
# System Files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
|
||||||
14
.nitro/types/nitro-config.d.ts
vendored
Normal file
14
.nitro/types/nitro-config.d.ts
vendored
Normal 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
136
.nitro/types/nitro-imports.d.ts
vendored
Normal 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
8
.nitro/types/nitro-routes.d.ts
vendored
Normal 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
3
.nitro/types/nitro.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
/// <reference path="./nitro-routes.d.ts" />
|
||||||
|
/// <reference path="./nitro-config.d.ts" />
|
||||||
|
/// <reference path="./nitro-imports.d.ts" />
|
||||||
|
|
@ -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.
|
Port admin modules one by one with strict API/permission parity.
|
||||||
|
|
||||||
See `docs/MIGRATION_MASTER_PLAN.md`.
|
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
8981
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
19
package.json
Normal file
19
package.json
Normal 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
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 664 B |
BIN
public/nxtgauge-icon.png
Normal file
BIN
public/nxtgauge-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
BIN
public/nxtgauge-logo.png
Normal file
BIN
public/nxtgauge-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
188
src/app.css
Normal file
188
src/app.css
Normal 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
20
src/app.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/components/AdminShell.tsx
Normal file
11
src/components/AdminShell.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
src/components/AdminSidebar.tsx
Normal file
25
src/components/AdminSidebar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/components/Counter.css
Normal file
21
src/components/Counter.css
Normal 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);
|
||||||
|
}
|
||||||
11
src/components/Counter.tsx
Normal file
11
src/components/Counter.tsx
Normal 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
4
src/entry-client.tsx
Normal 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
21
src/entry-server.tsx
Normal 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
1
src/global.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="@solidjs/start/env" />
|
||||||
45
src/lib/runtime/storage.ts
Normal file
45
src/lib/runtime/storage.ts
Normal 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
25
src/lib/runtime/types.ts
Normal 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
19
src/routes/[...404].tsx
Normal 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
10
src/routes/about.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Title } from "@solidjs/meta";
|
||||||
|
|
||||||
|
export default function About() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<Title>About</Title>
|
||||||
|
<h1>About</h1>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/routes/admin/index.tsx
Normal file
11
src/routes/admin/index.tsx
Normal 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;
|
||||||
|
}
|
||||||
87
src/routes/admin/onboarding-schemas/new.tsx
Normal file
87
src/routes/admin/onboarding-schemas/new.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
93
src/routes/admin/role-ui-configs/new.tsx
Normal file
93
src/routes/admin/role-ui-configs/new.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
src/routes/admin/runtime-roles/new.tsx
Normal file
80
src/routes/admin/runtime-roles/new.tsx
Normal 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
11
src/routes/index.tsx
Normal 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
19
tsconfig.json
Normal 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
3
vite.config.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { defineConfig } from '@solidjs/start/config';
|
||||||
|
|
||||||
|
export default defineConfig({});
|
||||||
Loading…
Add table
Reference in a new issue