mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-27 15:24:56 +00:00
feat: refactor Providers and theme management; implement Lua engine with execution and destruction capabilities
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { createContext } from 'react'
|
||||
import type { User } from '@/lib/level-types'
|
||||
|
||||
export interface AuthContextType {
|
||||
user: User | null
|
||||
isLoading: boolean
|
||||
login: (username: string, password: string) => Promise<void>
|
||||
logout: () => Promise<void>
|
||||
register: (username: string, email: string, password: string) => Promise<void>
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
@@ -1,70 +1,4 @@
|
||||
'use client'
|
||||
|
||||
import { ThemeProvider as MuiThemeProvider, CssBaseline } from '@mui/material'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useState, useMemo, createContext, useContext } from 'react'
|
||||
import { lightTheme, darkTheme } from '@/theme/mui-theme'
|
||||
|
||||
type ThemeMode = 'light' | 'dark' | 'system'
|
||||
|
||||
interface ThemeContextType {
|
||||
mode: ThemeMode
|
||||
setMode: (mode: ThemeMode) => void
|
||||
toggleTheme: () => void
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext)
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within Providers')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const [mode, setMode] = useState<ThemeMode>('system')
|
||||
|
||||
const theme = useMemo(() => {
|
||||
if (mode === 'system') {
|
||||
// Detect system preference
|
||||
const isDark = typeof window !== 'undefined'
|
||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
: false
|
||||
return isDark ? darkTheme : lightTheme
|
||||
}
|
||||
return mode === 'dark' ? darkTheme : lightTheme
|
||||
}, [mode])
|
||||
|
||||
const toggleTheme = () => {
|
||||
setMode(current => {
|
||||
if (current === 'light') return 'dark'
|
||||
if (current === 'dark') return 'system'
|
||||
return 'light'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ mode, setMode, toggleTheme }}>
|
||||
<MuiThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</MuiThemeProvider>
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
export { Providers } from './providers/providers-component'
|
||||
export { useTheme } from './providers/use-theme'
|
||||
|
||||
53
frontends/nextjs/src/app/providers/providers-component.tsx
Normal file
53
frontends/nextjs/src/app/providers/providers-component.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
import { CssBaseline, ThemeProvider as MuiThemeProvider } from '@mui/material'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { lightTheme, darkTheme } from '@/theme/mui-theme'
|
||||
import { ThemeContext, type ThemeMode } from './theme-context'
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const [mode, setMode] = useState<ThemeMode>('system')
|
||||
|
||||
const theme = useMemo(() => {
|
||||
if (mode === 'system') {
|
||||
// Detect system preference
|
||||
const isDark = typeof window !== 'undefined'
|
||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
: false
|
||||
return isDark ? darkTheme : lightTheme
|
||||
}
|
||||
return mode === 'dark' ? darkTheme : lightTheme
|
||||
}, [mode])
|
||||
|
||||
const toggleTheme = () => {
|
||||
setMode(current => {
|
||||
if (current === 'light') return 'dark'
|
||||
if (current === 'dark') return 'system'
|
||||
return 'light'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ mode, setMode, toggleTheme }}>
|
||||
<MuiThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</MuiThemeProvider>
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
11
frontends/nextjs/src/app/providers/theme-context.ts
Normal file
11
frontends/nextjs/src/app/providers/theme-context.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createContext } from 'react'
|
||||
|
||||
export type ThemeMode = 'light' | 'dark' | 'system'
|
||||
|
||||
export interface ThemeContextType {
|
||||
mode: ThemeMode
|
||||
setMode: (mode: ThemeMode) => void
|
||||
toggleTheme: () => void
|
||||
}
|
||||
|
||||
export const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
|
||||
10
frontends/nextjs/src/app/providers/use-theme.ts
Normal file
10
frontends/nextjs/src/app/providers/use-theme.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from 'react'
|
||||
import { ThemeContext } from './theme-context'
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext)
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within Providers')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -11,6 +11,9 @@ import { Database, CssCategory } from '@/lib/database'
|
||||
import { Plus, X, Pencil, Trash, FloppyDisk } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
const CLASS_TOKEN_PATTERN = /^[A-Za-z0-9:_/.[\]()%#!,=+-]+$/
|
||||
const uniqueClasses = (classes: string[]) => Array.from(new Set(classes))
|
||||
|
||||
export function CssClassManager() {
|
||||
const [categories, setCategories] = useState<CssCategory[]>([])
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
@@ -18,61 +21,121 @@ export function CssClassManager() {
|
||||
const [categoryName, setCategoryName] = useState('')
|
||||
const [classes, setClasses] = useState<string[]>([])
|
||||
const [newClass, setNewClass] = useState('')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [classSearchQuery, setClassSearchQuery] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
loadCategories()
|
||||
}, [])
|
||||
|
||||
const normalizedSearch = searchQuery.trim().toLowerCase()
|
||||
const filteredCategories = normalizedSearch
|
||||
? categories.filter((category) =>
|
||||
category.name.toLowerCase().includes(normalizedSearch) ||
|
||||
category.classes.some((cls) => cls.toLowerCase().includes(normalizedSearch))
|
||||
)
|
||||
: categories
|
||||
const totalClassCount = categories.reduce((total, category) => total + category.classes.length, 0)
|
||||
|
||||
const newClassTokens = newClass.trim().split(/\s+/).filter(Boolean)
|
||||
const uniqueNewClassTokens = Array.from(new Set(newClassTokens))
|
||||
const invalidNewClassTokens = uniqueNewClassTokens.filter((token) => !CLASS_TOKEN_PATTERN.test(token))
|
||||
const duplicateNewClassTokens = uniqueNewClassTokens.filter((token) => classes.includes(token))
|
||||
const canAddClass =
|
||||
uniqueNewClassTokens.length > 0 &&
|
||||
invalidNewClassTokens.length === 0 &&
|
||||
uniqueNewClassTokens.some((token) => !classes.includes(token))
|
||||
|
||||
const normalizedClassSearch = classSearchQuery.trim().toLowerCase()
|
||||
const filteredEditorClasses = normalizedClassSearch
|
||||
? classes.filter((cls) => cls.toLowerCase().includes(normalizedClassSearch))
|
||||
: classes
|
||||
|
||||
const loadCategories = async () => {
|
||||
const cats = await Database.getCssClasses()
|
||||
setCategories(cats)
|
||||
const normalized = cats.map((category) => ({
|
||||
...category,
|
||||
classes: uniqueClasses(category.classes),
|
||||
}))
|
||||
const sorted = normalized.slice().sort((a, b) => a.name.localeCompare(b.name))
|
||||
setCategories(sorted)
|
||||
}
|
||||
|
||||
const startEdit = (category?: CssCategory) => {
|
||||
if (category) {
|
||||
setEditingCategory(category)
|
||||
setCategoryName(category.name)
|
||||
setClasses([...category.classes])
|
||||
setClasses(uniqueClasses(category.classes))
|
||||
} else {
|
||||
setEditingCategory(null)
|
||||
setCategoryName('')
|
||||
setClasses([])
|
||||
}
|
||||
setNewClass('')
|
||||
setClassSearchQuery('')
|
||||
setIsEditing(true)
|
||||
}
|
||||
|
||||
const addClass = () => {
|
||||
if (newClass.trim()) {
|
||||
setClasses(current => [...current, newClass.trim()])
|
||||
setNewClass('')
|
||||
if (uniqueNewClassTokens.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (invalidNewClassTokens.length > 0) {
|
||||
toast.error(`Invalid class name: ${invalidNewClassTokens.join(', ')}`)
|
||||
return
|
||||
}
|
||||
|
||||
const newTokens = uniqueNewClassTokens.filter((token) => !classes.includes(token))
|
||||
if (newTokens.length === 0) {
|
||||
toast.info('Those classes already exist in this category')
|
||||
return
|
||||
}
|
||||
|
||||
setClasses((current) => uniqueClasses([...current, ...newTokens]))
|
||||
setNewClass('')
|
||||
}
|
||||
|
||||
const removeClass = (index: number) => {
|
||||
setClasses(current => current.filter((_, i) => i !== index))
|
||||
const removeClass = (cssClass: string) => {
|
||||
setClasses(current => current.filter((cls) => cls !== cssClass))
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!categoryName || classes.length === 0) {
|
||||
const trimmedName = categoryName.trim()
|
||||
const normalizedClasses = uniqueClasses(classes)
|
||||
if (!trimmedName || normalizedClasses.length === 0) {
|
||||
toast.error('Please provide a category name and at least one class')
|
||||
return
|
||||
}
|
||||
|
||||
const nameConflict = categories.some((category) =>
|
||||
category.name.toLowerCase() === trimmedName.toLowerCase() &&
|
||||
category.name !== editingCategory?.name
|
||||
)
|
||||
if (nameConflict) {
|
||||
toast.error('A category with this name already exists')
|
||||
return
|
||||
}
|
||||
|
||||
const newCategory: CssCategory = {
|
||||
name: categoryName,
|
||||
classes,
|
||||
name: trimmedName,
|
||||
classes: normalizedClasses,
|
||||
}
|
||||
|
||||
if (editingCategory) {
|
||||
await Database.updateCssCategory(categoryName, classes)
|
||||
toast.success('Category updated successfully')
|
||||
} else {
|
||||
await Database.addCssCategory(newCategory)
|
||||
toast.success('Category created successfully')
|
||||
}
|
||||
try {
|
||||
if (editingCategory) {
|
||||
await Database.updateCssCategory(editingCategory.name, newCategory)
|
||||
toast.success('Category updated successfully')
|
||||
} else {
|
||||
await Database.addCssCategory(newCategory)
|
||||
toast.success('Category created successfully')
|
||||
}
|
||||
|
||||
setIsEditing(false)
|
||||
loadCategories()
|
||||
setIsEditing(false)
|
||||
loadCategories()
|
||||
} catch (error) {
|
||||
toast.error('Failed to save category')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (categoryName: string) => {
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
import * as fengari from 'fengari-web'
|
||||
import type { LuaExecutionContext, LuaExecutionResult } from './functions/types'
|
||||
import { setupContextAPI } from './functions/setup/setup-context-api'
|
||||
import { executeLuaCode } from './functions/execution/execute-lua-code'
|
||||
import { execute } from './functions/engine/execute'
|
||||
import { destroy } from './functions/engine/destroy'
|
||||
|
||||
const lua = fengari.lua
|
||||
const lauxlib = fengari.lauxlib
|
||||
const lualib = fengari.lualib
|
||||
|
||||
@@ -26,8 +26,8 @@ export type { LuaExecutionContext, LuaExecutionResult }
|
||||
* LuaEngine class wraps individual Lua execution lambdas
|
||||
*/
|
||||
export class LuaEngine {
|
||||
private L: any
|
||||
private logs: string[] = []
|
||||
L: any
|
||||
logs: string[] = []
|
||||
|
||||
constructor() {
|
||||
this.L = lauxlib.luaL_newstate()
|
||||
@@ -35,33 +35,11 @@ export class LuaEngine {
|
||||
setupContextAPI(this.L, this.logs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Lua code with a context
|
||||
* @param code - Lua code to execute
|
||||
* @param context - Execution context
|
||||
* @returns Execution result
|
||||
*/
|
||||
async execute(code: string, context: LuaExecutionContext = {}): Promise<LuaExecutionResult> {
|
||||
this.logs.length = 0
|
||||
return executeLuaCode(this.L, code, context, this.logs)
|
||||
}
|
||||
/** Execute Lua code with a context */
|
||||
execute = execute
|
||||
|
||||
/**
|
||||
* Destroy the Lua state
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.L) {
|
||||
lua.lua_close(this.L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create a new LuaEngine instance
|
||||
* @returns New LuaEngine instance
|
||||
*/
|
||||
export const createLuaEngine = (): LuaEngine => {
|
||||
return new LuaEngine()
|
||||
/** Destroy the Lua state */
|
||||
destroy = destroy
|
||||
}
|
||||
|
||||
// Re-export individual functions for direct imports
|
||||
|
||||
9
frontends/nextjs/src/lib/lua/create-lua-engine.ts
Normal file
9
frontends/nextjs/src/lib/lua/create-lua-engine.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { LuaEngine } from './LuaEngine'
|
||||
|
||||
/**
|
||||
* Factory function to create a new LuaEngine instance
|
||||
* @returns New LuaEngine instance
|
||||
*/
|
||||
export const createLuaEngine = (): LuaEngine => {
|
||||
return new LuaEngine()
|
||||
}
|
||||
13
frontends/nextjs/src/lib/lua/functions/engine/destroy.ts
Normal file
13
frontends/nextjs/src/lib/lua/functions/engine/destroy.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import * as fengari from 'fengari-web'
|
||||
import type { LuaEngine } from '../../LuaEngine'
|
||||
|
||||
const lua = fengari.lua
|
||||
|
||||
/**
|
||||
* Destroy the Lua state
|
||||
*/
|
||||
export function destroy(this: LuaEngine): void {
|
||||
if (this.L) {
|
||||
lua.lua_close(this.L)
|
||||
}
|
||||
}
|
||||
18
frontends/nextjs/src/lib/lua/functions/engine/execute.ts
Normal file
18
frontends/nextjs/src/lib/lua/functions/engine/execute.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { LuaExecutionContext, LuaExecutionResult } from '../types'
|
||||
import type { LuaEngine } from '../../LuaEngine'
|
||||
import { executeLuaCode } from '../execution/execute-lua-code'
|
||||
|
||||
/**
|
||||
* Execute Lua code with a context
|
||||
* @param code - Lua code to execute
|
||||
* @param context - Execution context
|
||||
* @returns Execution result
|
||||
*/
|
||||
export async function execute(
|
||||
this: LuaEngine,
|
||||
code: string,
|
||||
context: LuaExecutionContext = {}
|
||||
): Promise<LuaExecutionResult> {
|
||||
this.logs.length = 0
|
||||
return executeLuaCode(this.L, code, context, this.logs)
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from './LuaEngine'
|
||||
export { createLuaEngine } from './create-lua-engine'
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { packageGlue, type PackageGlue } from './package-glue'
|
||||
|
||||
export const getPackageGlue = (): PackageGlue => packageGlue
|
||||
@@ -35,33 +35,5 @@ export {
|
||||
isPackageInstalled,
|
||||
uninstallPackage,
|
||||
}
|
||||
|
||||
/**
|
||||
* PackageGlue - Wrapper class for package registry helpers
|
||||
*
|
||||
* Each method delegates to a single-function module.
|
||||
*/
|
||||
export class PackageGlue {
|
||||
buildPackageRegistry = buildPackageRegistry
|
||||
getPackage = getPackage
|
||||
getPackagesByCategory = getPackagesByCategory
|
||||
getPackageComponents = getPackageComponents
|
||||
getPackageScripts = getPackageScripts
|
||||
getPackageScriptFiles = getPackageScriptFiles
|
||||
getAllPackageScripts = getAllPackageScripts
|
||||
getPackageExamples = getPackageExamples
|
||||
checkDependencies = checkDependencies
|
||||
installPackageComponents = installPackageComponents
|
||||
installPackageScripts = installPackageScripts
|
||||
installPackage = installPackage
|
||||
uninstallPackage = uninstallPackage
|
||||
getInstalledPackages = getInstalledPackages
|
||||
isPackageInstalled = isPackageInstalled
|
||||
exportAllPackagesForSeed = exportAllPackagesForSeed
|
||||
}
|
||||
|
||||
export const packageGlue = new PackageGlue()
|
||||
|
||||
export function getPackageGlue(): PackageGlue {
|
||||
return packageGlue
|
||||
}
|
||||
export { PackageGlue, packageGlue } from './package-glue'
|
||||
export { getPackageGlue } from './get-package-glue'
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { buildPackageRegistry } from './build-package-registry'
|
||||
import { checkDependencies } from './check-dependencies'
|
||||
import { exportAllPackagesForSeed } from './export-all-packages-for-seed'
|
||||
import { getAllPackageScripts } from './get-all-package-scripts'
|
||||
import { getInstalledPackages } from './get-installed-packages'
|
||||
import { getPackage } from './get-package'
|
||||
import { getPackageComponents } from './get-package-components'
|
||||
import { getPackageExamples } from './get-package-examples'
|
||||
import { getPackageScriptFiles } from './get-package-script-files'
|
||||
import { getPackageScripts } from './get-package-scripts'
|
||||
import { getPackagesByCategory } from './get-packages-by-category'
|
||||
import { installPackage } from './install-package'
|
||||
import { installPackageComponents } from './install-package-components'
|
||||
import { installPackageScripts } from './install-package-scripts'
|
||||
import { isPackageInstalled } from './is-package-installed'
|
||||
import { uninstallPackage } from './uninstall-package'
|
||||
|
||||
/**
|
||||
* PackageGlue - Wrapper class for package registry helpers
|
||||
*
|
||||
* Each method delegates to a single-function module.
|
||||
*/
|
||||
export class PackageGlue {
|
||||
buildPackageRegistry = buildPackageRegistry
|
||||
getPackage = getPackage
|
||||
getPackagesByCategory = getPackagesByCategory
|
||||
getPackageComponents = getPackageComponents
|
||||
getPackageScripts = getPackageScripts
|
||||
getPackageScriptFiles = getPackageScriptFiles
|
||||
getAllPackageScripts = getAllPackageScripts
|
||||
getPackageExamples = getPackageExamples
|
||||
checkDependencies = checkDependencies
|
||||
installPackageComponents = installPackageComponents
|
||||
installPackageScripts = installPackageScripts
|
||||
installPackage = installPackage
|
||||
uninstallPackage = uninstallPackage
|
||||
getInstalledPackages = getInstalledPackages
|
||||
isPackageInstalled = isPackageInstalled
|
||||
exportAllPackagesForSeed = exportAllPackagesForSeed
|
||||
}
|
||||
|
||||
export const packageGlue = new PackageGlue()
|
||||
Reference in New Issue
Block a user