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
|
||||
.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
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.
|
||||
|
||||
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