reimpl of some missing features from before the deletions of everything
This commit is contained in:
parent
e9a3f8ca09
commit
67c326ad3b
BIN
project-warstone/bun.lockb
Executable file
BIN
project-warstone/bun.lockb
Executable file
Binary file not shown.
@ -17,7 +17,7 @@
|
|||||||
"postcss": "^8.4.24",
|
"postcss": "^8.4.24",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.12.0",
|
"react-router-dom": "^6.21.0",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
"recoilize": "^3.2.0",
|
"recoilize": "^3.2.0",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
Start query: ?
|
Start query: ?
|
||||||
- by default, queries search the publication
|
- by default, queries search the current publication
|
||||||
|
- you can specify a publication by following this with the name, by default it will use the latest release of a publication
|
||||||
|
- you can specify the version by including it in a pair of square brackets
|
||||||
Query current object: ^
|
Query current object: ^
|
||||||
- refers specifically to the object based off of this specific type
|
- refers specifically to the object based off of this specific type
|
||||||
Query relative object: $
|
Query relative object: $
|
||||||
- refers to the last relative object in the heirarchy
|
- refers to the last relative object in the hierarchy
|
||||||
Access child: .
|
Access child: .
|
||||||
- this also maps over all items in a list and returns a list of the relevant children
|
- this also maps over all items in a list and returns a list of the relevant children
|
||||||
- if the parent is a list, it will return a list of just the specified child
|
- if the parent is a list, it will return a list of just the specified child
|
||||||
@ -45,6 +47,8 @@ separator: ::;;
|
|||||||
Examples:
|
Examples:
|
||||||
?core.weapon_abilities[name=Rapid Fire].body
|
?core.weapon_abilities[name=Rapid Fire].body
|
||||||
- this searches the publication 'core' object for weapon_abilities that have the name
|
- this searches the publication 'core' object for weapon_abilities that have the name
|
||||||
|
?core[v1].weapon_abilities[name=Rapid Fire].body
|
||||||
|
- this searches version 'v1' of the publication 'core' object for weapon_abilities that have the name
|
||||||
Sustained Hits {{?^sustained_hits}}
|
Sustained Hits {{?^sustained_hits}}
|
||||||
- This formats the string with the queried values, which start the query within the same object that the field exists in. This would result in the string 'Sustained Hits 1'
|
- This formats the string with the queried values, which start the query within the same object that the field exists in. This would result in the string 'Sustained Hits 1'
|
||||||
Anti-{{_.keyword}} {{_.value}}<<?^anti_abilities
|
Anti-{{_.keyword}} {{_.value}}<<?^anti_abilities
|
@ -2,13 +2,22 @@ import { RecoilRoot } from 'recoil'
|
|||||||
import { SchemaBuilder } from './components/SchemaBuilder'
|
import { SchemaBuilder } from './components/SchemaBuilder'
|
||||||
import { GameSystemEditor } from './components/GameSystemEditor'
|
import { GameSystemEditor } from './components/GameSystemEditor'
|
||||||
import RecoilizeDebugger from 'recoilize';
|
import RecoilizeDebugger from 'recoilize';
|
||||||
|
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||||
|
import { SchemaList } from './components/schemalist';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<RecoilRoot>
|
<RecoilRoot>
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route Component={SchemaList} path='/schemas' />
|
||||||
|
<Route Component={SchemaBuilder} path='/schema/:id' />
|
||||||
|
{/* <Route Component={SchemaBuilder} path='/schema/new' /> */}
|
||||||
|
<Route Component={GameSystemEditor} path='/game-system/:id' />
|
||||||
|
{/* <Route Component={GameSystemEditor} path='/game-system/new' /> */}
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
<RecoilizeDebugger />
|
<RecoilizeDebugger />
|
||||||
<SchemaBuilder />
|
|
||||||
{/* <GameSystemEditor /> */}
|
|
||||||
</RecoilRoot>
|
</RecoilRoot>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -16,19 +16,20 @@ export const GameSystemEditor: FC = () => {
|
|||||||
name: 'Asshammer 40x a day',
|
name: 'Asshammer 40x a day',
|
||||||
accolades: [],
|
accolades: [],
|
||||||
});
|
});
|
||||||
const [schemas, setSchemas] = useState<{ name: string, id: string }[]>([]);
|
const [schemas, setSchemas] = useState<[string,string][]>([]);
|
||||||
|
|
||||||
const [lastSaved, setLastSaved] = useState(gameSystem);
|
const [lastSaved, setLastSaved] = useState(gameSystem);
|
||||||
|
|
||||||
const fetchSchema = useCallback(async (id: string) => {
|
const fetchSchema = useCallback(async (id: string) => {
|
||||||
const res = await GameSystemsService.getSchema(id);
|
try {
|
||||||
|
const schema = await GameSystemsService.getSchema(id);
|
||||||
|
|
||||||
if (res.status !== 200) return;
|
updateGameSystem({
|
||||||
const schema = await res.json()
|
schema
|
||||||
|
});
|
||||||
updateGameSystem({
|
} catch (e) {
|
||||||
schema
|
console.log('failed to fetch schema:', e)
|
||||||
});
|
}
|
||||||
}, [updateGameSystem])
|
}, [updateGameSystem])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -38,7 +39,6 @@ export const GameSystemEditor: FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
GameSystemsService.getSchemaList()
|
GameSystemsService.getSchemaList()
|
||||||
.then(res => res.json())
|
|
||||||
.then(schemas => setSchemas(schemas));
|
.then(schemas => setSchemas(schemas));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -48,8 +48,7 @@ export const GameSystemEditor: FC = () => {
|
|||||||
}, [gameSystem])
|
}, [gameSystem])
|
||||||
|
|
||||||
const fetchGameSystem = useCallback(async () => {
|
const fetchGameSystem = useCallback(async () => {
|
||||||
const res = await GameSystemsService.getGameSystem('');
|
const gs = await GameSystemsService.getGameSystem('');
|
||||||
const gs = await res.json();
|
|
||||||
setGameSystem(gs);
|
setGameSystem(gs);
|
||||||
setLastSaved(gs);
|
setLastSaved(gs);
|
||||||
}, [setGameSystem]);
|
}, [setGameSystem]);
|
||||||
@ -66,7 +65,7 @@ export const GameSystemEditor: FC = () => {
|
|||||||
<select className="no-default text-white bg-transparent header border-b-2 border-falcon" {...bindGameSystemProperty('schemaId')}>
|
<select className="no-default text-white bg-transparent header border-b-2 border-falcon" {...bindGameSystemProperty('schemaId')}>
|
||||||
<option value=""></option>
|
<option value=""></option>
|
||||||
{schemas.map(s => (
|
{schemas.map(s => (
|
||||||
<option value={s.id}>{s.name}</option>
|
<option value={s[0]}>{s[1]}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<button onClick={saveGameSystem} disabled={lastSaved === gameSystem}>Save Game System</button>
|
<button onClick={saveGameSystem} disabled={lastSaved === gameSystem}>Save Game System</button>
|
||||||
|
@ -24,9 +24,9 @@ export const FieldEditor: FC<IProps> = ({ update, field, fieldName, deleteField
|
|||||||
setReserved(RESERVED_FIELDS[fieldName]);
|
setReserved(RESERVED_FIELDS[fieldName]);
|
||||||
}, [fieldName])
|
}, [fieldName])
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
console.log(field.value);
|
// console.log(field.value);
|
||||||
}, [field])
|
// }, [field])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="odd:bg-black/50">
|
<li className="odd:bg-black/50">
|
||||||
|
@ -4,17 +4,23 @@ import { TypeEditor } from './type-editor';
|
|||||||
import { useObjectState, useObjectStateWrapper } from '../../hooks/useObjectState';
|
import { useObjectState, useObjectStateWrapper } from '../../hooks/useObjectState';
|
||||||
import { FieldTypes, Schema, Template, TypeType } from '../../types/schema';
|
import { FieldTypes, Schema, Template, TypeType } from '../../types/schema';
|
||||||
import { useInput } from '../../hooks/useInput';
|
import { useInput } from '../../hooks/useInput';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState, useResetRecoilState } from 'recoil';
|
||||||
import { SchemaEditAtom } from '../../recoil/atoms/schema';
|
import { SchemaEditAtom } from '../../recoil/atoms/schema';
|
||||||
import { GameSystemsService } from '../../services/game-systems';
|
import { GameSystemsService } from '../../services/game-systems';
|
||||||
import { SchemaViewer } from './schema-viewer';
|
import { SchemaViewer } from './schema-viewer';
|
||||||
import { TemplateEditor } from './template-editor';
|
import { TemplateEditor } from './template-editor';
|
||||||
import { Icon } from '../Icon';
|
import { Icon } from '../Icon';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
||||||
export const SchemaBuilder: FC = () => {
|
export const SchemaBuilder: FC = () => {
|
||||||
const [schema, setSchema] = useRecoilState(SchemaEditAtom);
|
const [schema, setSchema] = useRecoilState(SchemaEditAtom);
|
||||||
const { update: updateSchema } = useObjectStateWrapper<Schema>(schema, setSchema);
|
const resetSchema = useResetRecoilState(SchemaEditAtom);
|
||||||
|
const { update: updateSchema, bindProperty:bindSchemaProperty } = useObjectStateWrapper<Schema>(schema, setSchema);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const {id} = useParams<{id: string}>()
|
||||||
|
|
||||||
const { value: typeName, bind: bindTypeName, reset: resetTypeName } = useInput('');
|
const { value: typeName, bind: bindTypeName, reset: resetTypeName } = useInput('');
|
||||||
|
|
||||||
@ -23,19 +29,18 @@ export const SchemaBuilder: FC = () => {
|
|||||||
const [lastSaved, setLastSaved] = useState(schema);
|
const [lastSaved, setLastSaved] = useState(schema);
|
||||||
|
|
||||||
const fetchSchema = useCallback(async () => {
|
const fetchSchema = useCallback(async () => {
|
||||||
const result = await GameSystemsService.getSchema('286f4c18-d280-444b-8d7e-9a3dd09f64ef')
|
if (!id) return;
|
||||||
if (result.status !== 200) return;
|
if (id === 'new') return resetSchema();
|
||||||
const fetchedSchema = await result.json();
|
const fetchedSchema = await GameSystemsService.getSchema(id)
|
||||||
// if (fetchedSchema.name === schema.name) return;
|
// if (fetchedSchema.name === schema.name) return;
|
||||||
if (!fetchedSchema.templates) fetchedSchema.templates = {}
|
if (!fetchedSchema.templates) fetchedSchema.templates = {}
|
||||||
setSchema(fetchedSchema);
|
setSchema(fetchedSchema);
|
||||||
setLastSaved(fetchedSchema);
|
setLastSaved(fetchedSchema);
|
||||||
}, [setSchema])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSchema();
|
fetchSchema();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [fetchSchema])
|
||||||
}, [])
|
|
||||||
|
|
||||||
const [selectedType, setSelectedType] = useState('');
|
const [selectedType, setSelectedType] = useState('');
|
||||||
|
|
||||||
@ -51,9 +56,10 @@ export const SchemaBuilder: FC = () => {
|
|||||||
setSelectedType('');
|
setSelectedType('');
|
||||||
}, [resetTypeName, updateSchema]);
|
}, [resetTypeName, updateSchema]);
|
||||||
|
|
||||||
const saveSchema = useCallback(() => {
|
const saveSchema = useCallback(async () => {
|
||||||
GameSystemsService.saveSchema(schema);
|
|
||||||
setLastSaved(schema);
|
setLastSaved(schema);
|
||||||
|
const sid = await GameSystemsService.saveSchema(schema);
|
||||||
|
if (id === 'new') navigate('/schema/'+sid)
|
||||||
}, [schema])
|
}, [schema])
|
||||||
|
|
||||||
const selectTypeForEdit = useCallback((typeKey: string) => {
|
const selectTypeForEdit = useCallback((typeKey: string) => {
|
||||||
@ -95,6 +101,9 @@ export const SchemaBuilder: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="container flex gap-4 p-8">
|
<div className="container flex gap-4 p-8">
|
||||||
<div className="panel w-2/3 h-full flex flex-col gap-4">
|
<div className="panel w-2/3 h-full flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<input type="text" {...bindSchemaProperty('name')} placeholder="Schema Name" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="subheader mb-2">Add a template</p>
|
<p className="subheader mb-2">Add a template</p>
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
@ -103,7 +112,7 @@ export const SchemaBuilder: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<ul className="rounded-lg overflow-hidden">
|
<ul className="rounded-lg overflow-hidden">
|
||||||
{Object.entries(schema.templates).map(([templateKey, template]) => (
|
{Object.entries(schema.templates).map(([templateKey, template]) => (
|
||||||
<TemplateEditor templateKey={templateKey} template={template} update={updateTemplate} />
|
<TemplateEditor key={templateKey} templateKey={templateKey} template={template} update={updateTemplate} />
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -119,7 +128,7 @@ export const SchemaBuilder: FC = () => {
|
|||||||
</AnimatedPageContainer>
|
</AnimatedPageContainer>
|
||||||
<ul className="mt-3 w-96">
|
<ul className="mt-3 w-96">
|
||||||
{Object.keys(schema.types).map(t => (
|
{Object.keys(schema.types).map(t => (
|
||||||
<li className="odd:bg-black/50 flex justify-between p-2">
|
<li key={'type' + t} className="odd:bg-black/50 flex justify-between p-2">
|
||||||
{t}
|
{t}
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<button title="Edit" className="no-default" onClick={() => selectTypeForEdit(t)}>
|
<button title="Edit" className="no-default" onClick={() => selectTypeForEdit(t)}>
|
||||||
|
@ -40,10 +40,10 @@ export const TemplateEditor: FC<IProps> = ({ templateKey, update, template }) =>
|
|||||||
<input type="text" {...bindProperty('type', { disallowSpaces: true })} list="type-editor-type-list" />
|
<input type="text" {...bindProperty('type', { disallowSpaces: true })} list="type-editor-type-list" />
|
||||||
<datalist id="type-editor-type-list">
|
<datalist id="type-editor-type-list">
|
||||||
{Object.keys(TEMPLATE_TYPES).map(k => (
|
{Object.keys(TEMPLATE_TYPES).map(k => (
|
||||||
<option value={k}>{k}</option>
|
<option key={'templatetype' + k} value={k}>{k}</option>
|
||||||
))}
|
))}
|
||||||
{Object.keys(schema.types).map(k => (
|
{Object.keys(schema.types).map(k => (
|
||||||
<option value={k}>{k}</option>
|
<option key={'schematype' + k} value={k}>{k}</option>
|
||||||
))}
|
))}
|
||||||
</datalist>
|
</datalist>
|
||||||
</label>
|
</label>
|
||||||
|
@ -43,7 +43,7 @@ export const ValueField: FC<IValueProps> = ({ type, bind }) => {
|
|||||||
>
|
>
|
||||||
<option value=""></option>
|
<option value=""></option>
|
||||||
{DICE_SIDES.map(d => (
|
{DICE_SIDES.map(d => (
|
||||||
<option value={'d' + d}>{d}</option>
|
<option key={'dice sides' + d} value={'d' + d}>{d}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
20
project-warstone/src/components/schemalist.tsx
Normal file
20
project-warstone/src/components/schemalist.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { FC, useEffect, useState } from "react";
|
||||||
|
import { GameSystemsService } from "../services/game-systems";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
export const SchemaList: FC = () => {
|
||||||
|
const [schemas, setSchemas] = useState<[string,string][]>([]);
|
||||||
|
useEffect(() => {
|
||||||
|
GameSystemsService.getSchemaList().then(l => {
|
||||||
|
setSchemas(l)
|
||||||
|
});
|
||||||
|
}, [])
|
||||||
|
return (
|
||||||
|
<div className="container pt-12 flex flex-col gap-6">
|
||||||
|
<Link to="/schema/new"><button>New Schema</button></Link>
|
||||||
|
<ul className="panel">
|
||||||
|
{schemas.map(([id,name])=> <li key={id}><Link to={'/schema/' + id}><span className="w-full">{name || 'Unnamed Schema'}</span></Link></li>)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { FieldTypes, TypeType } from '../types/schema';
|
import { FieldTypes, TypeType } from "../types/schema";
|
||||||
|
|
||||||
export const TEMPLATE_TYPES: Record<string, TypeType> = {
|
export const TEMPLATE_TYPES: Record<string, TypeType> = {
|
||||||
section: {
|
section: {
|
||||||
@ -7,14 +7,14 @@ export const TEMPLATE_TYPES: Record<string, TypeType> = {
|
|||||||
limit: 1,
|
limit: 1,
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
type: FieldTypes.text,
|
type: FieldTypes.text,
|
||||||
value: ''
|
value: "",
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
isConstant: false,
|
isConstant: false,
|
||||||
limit: 0,
|
limit: 0,
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
type: FieldTypes['long text'],
|
type: FieldTypes["long text"],
|
||||||
value: ''
|
value: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
steps: {
|
steps: {
|
||||||
@ -23,8 +23,8 @@ export const TEMPLATE_TYPES: Record<string, TypeType> = {
|
|||||||
limit: 0,
|
limit: 0,
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
type: FieldTypes.type,
|
type: FieldTypes.type,
|
||||||
value: 'section'
|
value: "section",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
name: {
|
name: {
|
||||||
@ -32,14 +32,14 @@ export const TEMPLATE_TYPES: Record<string, TypeType> = {
|
|||||||
limit: 1,
|
limit: 1,
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
type: FieldTypes.text,
|
type: FieldTypes.text,
|
||||||
value: ''
|
value: "",
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
isConstant: false,
|
isConstant: false,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
type: FieldTypes.text,
|
type: FieldTypes.text,
|
||||||
value: ''
|
value: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
@ -47,18 +47,34 @@ export const TEMPLATE_TYPES: Record<string, TypeType> = {
|
|||||||
isConstant: false,
|
isConstant: false,
|
||||||
limit: 0,
|
limit: 0,
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
type: FieldTypes['long text'],
|
type: FieldTypes["long text"],
|
||||||
value: ''
|
value: "",
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
table_column: {
|
||||||
|
name: {
|
||||||
|
isConstant: false,
|
||||||
|
limit: 0,
|
||||||
|
minimum: 1,
|
||||||
|
type: FieldTypes.any,
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
isConstant: false,
|
||||||
|
limit: 0,
|
||||||
|
minimum: 1,
|
||||||
|
type: FieldTypes.any,
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
table_row: {
|
table_row: {
|
||||||
columns: {
|
columns: {
|
||||||
isConstant: false,
|
isConstant: false,
|
||||||
limit: 0,
|
limit: 0,
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
type: FieldTypes.any,
|
type: FieldTypes.type,
|
||||||
value: ''
|
value: "tableColumn",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
table: {
|
table: {
|
||||||
rows: {
|
rows: {
|
||||||
@ -66,14 +82,14 @@ export const TEMPLATE_TYPES: Record<string, TypeType> = {
|
|||||||
limit: 0,
|
limit: 0,
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
type: FieldTypes.type,
|
type: FieldTypes.type,
|
||||||
value: 'tableRow'
|
value: "tableRow",
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
isConstant: false,
|
isConstant: false,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
minimum: 0,
|
minimum: 0,
|
||||||
type: FieldTypes.type,
|
type: FieldTypes.type,
|
||||||
value: 'tableRow'
|
value: "tableRow",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
@ -1,46 +1,58 @@
|
|||||||
import React, { ChangeEvent, useCallback, useState } from 'react';
|
import React, { ChangeEvent, useCallback, useState } from "react";
|
||||||
import { InputBinder } from '../types/inputBinder';
|
import { InputBinder } from "../types/inputBinder";
|
||||||
|
|
||||||
type ObjectStateHookConfig = {
|
type ObjectStateHookConfig = {
|
||||||
disallowSpaces?: boolean;
|
disallowSpaces?: boolean;
|
||||||
spaceReplacer?: string;
|
spaceReplacer?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useObjectState = <T extends object>(initial: T) => {
|
export const useObjectState = <T extends object>(initial: T) => {
|
||||||
const [state, setState] = useState<T>(initial || {} as T);
|
const [state, setState] = useState<T>(initial || {} as T);
|
||||||
|
|
||||||
const bindProperty = useCallback(<K extends keyof T>(property: K, config: ObjectStateHookConfig) => ({
|
const bindProperty = useCallback(
|
||||||
value: state[property] ?? '',
|
<K extends keyof T>(property: K, config?: ObjectStateHookConfig) => ({
|
||||||
name: property,
|
value: state[property] ?? "",
|
||||||
onChange: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>
|
name: property,
|
||||||
setState(value => (
|
onChange: (
|
||||||
{
|
event: ChangeEvent<
|
||||||
...value,
|
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
||||||
[event.target.name]: (
|
>,
|
||||||
(typeof value[property] === 'number') ?
|
) =>
|
||||||
Number(event.target.value) || 0 :
|
setState((value) => (
|
||||||
config?.disallowSpaces ?
|
{
|
||||||
event.target.value.replace(' ', config.spaceReplacer || '_') :
|
...value,
|
||||||
event.target.value
|
[event.target.name]: (
|
||||||
)
|
(typeof value[property] === "number")
|
||||||
}
|
? Number(event.target.value) || 0
|
||||||
))
|
: config?.disallowSpaces
|
||||||
}), [state])
|
? event.target.value.replace(" ", config.spaceReplacer || "_")
|
||||||
|
: event.target.value
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
[state],
|
||||||
|
);
|
||||||
|
|
||||||
const bindPropertyCheck = useCallback(<K extends keyof T>(property: K) => ({
|
const bindPropertyCheck = useCallback(<K extends keyof T>(property: K) => ({
|
||||||
checked: !!state[property],
|
checked: !!state[property],
|
||||||
name: property,
|
name: property,
|
||||||
onChange: (event: ChangeEvent<HTMLInputElement>) => setState(value => ({
|
onChange: (event: ChangeEvent<HTMLInputElement>) =>
|
||||||
...value, [event.target.name]: (event.target.checked)
|
setState((value) => ({
|
||||||
})),
|
...value,
|
||||||
readOnly: true
|
[event.target.name]: (event.target.checked),
|
||||||
}), [state])
|
})),
|
||||||
|
readOnly: true,
|
||||||
|
}), [state]);
|
||||||
|
|
||||||
const update = useCallback((updates: Partial<T>) => setState(s => ({ ...s, ...updates })), [])
|
const update = useCallback(
|
||||||
|
(updates: Partial<T>) => setState((s) => ({ ...s, ...updates })),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
setState(initial);
|
setState(initial);
|
||||||
}, [initial])
|
}, [initial]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bindProperty,
|
bindProperty,
|
||||||
@ -48,48 +60,67 @@ export const useObjectState = <T extends object>(initial: T) => {
|
|||||||
update,
|
update,
|
||||||
state,
|
state,
|
||||||
setState,
|
setState,
|
||||||
reset
|
reset,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useObjectStateWrapper = <T extends object>(state: T, setState: React.Dispatch<React.SetStateAction<T>>) => {
|
export const useObjectStateWrapper = <T extends object>(
|
||||||
|
state: T,
|
||||||
const bindProperty = useCallback(<K extends keyof T>(property: K, config?: ObjectStateHookConfig): InputBinder => ({
|
setState: React.Dispatch<React.SetStateAction<T>>,
|
||||||
value: state[property] ?? '',
|
) => {
|
||||||
name: property.toString(),
|
const bindProperty = useCallback(
|
||||||
onChange: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>
|
<K extends keyof T>(
|
||||||
setState(value => (
|
property: K,
|
||||||
{
|
config?: ObjectStateHookConfig,
|
||||||
...value,
|
): InputBinder => ({
|
||||||
[event.target.name]: (
|
value: state[property]?.toString() ?? "",
|
||||||
(typeof value[property] === 'number') ?
|
name: property.toString(),
|
||||||
Number(event.target.value) || 0 :
|
onChange: (
|
||||||
config?.disallowSpaces ?
|
event: ChangeEvent<
|
||||||
event.target.value.replace(' ', config.spaceReplacer || '_') :
|
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
||||||
event.target.value
|
>,
|
||||||
)
|
) =>
|
||||||
}
|
setState((value) => (
|
||||||
))
|
{
|
||||||
}), [setState, state])
|
...value,
|
||||||
|
[event.target.name]: (
|
||||||
|
(typeof value[property] === "number")
|
||||||
|
? Number(event.target.value) || 0
|
||||||
|
: config?.disallowSpaces
|
||||||
|
? event.target.value.replace(" ", config.spaceReplacer || "_")
|
||||||
|
: event.target.value
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
[setState, state],
|
||||||
|
);
|
||||||
|
|
||||||
const bindPropertyCheck = useCallback(<K extends keyof T>(property: K) => ({
|
const bindPropertyCheck = useCallback(<K extends keyof T>(property: K) => ({
|
||||||
checked: !!state[property],
|
checked: !!state[property],
|
||||||
name: property,
|
name: property,
|
||||||
onChange: (event: ChangeEvent<HTMLInputElement>) => setState(value => ({
|
onChange: (event: ChangeEvent<HTMLInputElement>) =>
|
||||||
...value, [event.target.name]: (event.target.checked)
|
setState((value) => ({
|
||||||
})),
|
...value,
|
||||||
readOnly: true
|
[event.target.name]: (event.target.checked),
|
||||||
}), [setState, state])
|
})),
|
||||||
|
readOnly: true,
|
||||||
|
}), [setState, state]);
|
||||||
|
|
||||||
const update = useCallback((updates: Partial<T> | ((arg: T) => Partial<T>)) =>
|
const update = useCallback(
|
||||||
setState(s => ({ ...s, ...(typeof updates === 'function' ? updates(s) : updates) }
|
(updates: Partial<T> | ((arg: T) => Partial<T>)) =>
|
||||||
)), [setState])
|
setState((s) => ({
|
||||||
|
...s,
|
||||||
|
...(typeof updates === "function" ? updates(s) : updates),
|
||||||
|
})),
|
||||||
|
[setState],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bindProperty,
|
bindProperty,
|
||||||
bindPropertyCheck,
|
bindPropertyCheck,
|
||||||
update,
|
update,
|
||||||
state,
|
state,
|
||||||
setState
|
setState,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
@ -13,7 +13,7 @@ export const Accordion: FCC<IProps> = ({ children, expandOnHover, expanded, titl
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-expanded={open || expanded}
|
data-expanded={open || expanded}
|
||||||
data-expandOnHover={expandOnHover}
|
data-expandonhover={expandOnHover}
|
||||||
className={(expandOnHover ? 'group/hover' : 'group/controlled') + ' group'}
|
className={(expandOnHover ? 'group/hover' : 'group/controlled') + ' group'}
|
||||||
onClick={() => !title && !expandOnHover && setOpen(!open)}
|
onClick={() => !title && !expandOnHover && setOpen(!open)}
|
||||||
>
|
>
|
||||||
|
@ -1,51 +1,45 @@
|
|||||||
import { GameSystem } from '../types/gameSystem';
|
import { GameSystem } from "../types/gameSystem";
|
||||||
import { Schema } from '../types/schema';
|
import { Schema } from "../types/schema";
|
||||||
|
import { idb } from "./indexeddb";
|
||||||
|
|
||||||
const emptySchema = { name: '', types: {}, templates: {}, id: crypto.randomUUID() };
|
const emptySchema = {
|
||||||
|
name: "",
|
||||||
|
types: {},
|
||||||
|
templates: {},
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
};
|
||||||
|
|
||||||
export const GameSystemsService = {
|
export const GameSystemsService = {
|
||||||
// todo - connect to service to save schema for game
|
// todo - connect to service to save schema for game
|
||||||
saveSchema: async (schema: Schema) => {
|
saveSchema: async (schema: Schema) => {
|
||||||
localStorage.setItem('schema ' + schema.id, JSON.stringify(schema));
|
// localStorage.setItem('schema ' + schema.id, JSON.stringify(schema));
|
||||||
|
try {
|
||||||
return { status: 200 }
|
return await idb.createOrUpdate({
|
||||||
|
storeName: "schema",
|
||||||
|
...schema,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// todo - connect to service to fetch schema for game
|
// todo - connect to service to fetch schema for game
|
||||||
getSchema: async (id: string): Promise<{ status: 200 | 404, json: () => Promise<Schema> }> => {
|
getSchema: async (
|
||||||
const schema = localStorage.getItem('schema ' + id);
|
id: string,
|
||||||
|
): Promise<Schema> => {
|
||||||
if (schema)
|
try {
|
||||||
return {
|
const schema = await idb.read("schema", id);
|
||||||
status: 200,
|
return schema;
|
||||||
json: async () => JSON.parse(schema)
|
} catch (e) {
|
||||||
}
|
throw e;
|
||||||
|
|
||||||
return {
|
|
||||||
status: 404,
|
|
||||||
json: async () => (emptySchema)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getSchemaList: async () => {
|
|
||||||
return {
|
|
||||||
status: 200, json: async () => [{
|
|
||||||
name: 'Test Schema',
|
|
||||||
id: '286f4c18-d280-444b-8d7e-9a3dd09f64ef'
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getSchemaList: async () => await idb.listAll<string>("schema", "name"),
|
||||||
|
|
||||||
saveGameSystem: async (gs: GameSystem) => {
|
saveGameSystem: async (gs: GameSystem) => {
|
||||||
localStorage.setItem('game-system ' + gs.id, JSON.stringify(gs));
|
localStorage.setItem("game-system " + gs.id, JSON.stringify(gs));
|
||||||
return { status: 200 }
|
return { status: 200 };
|
||||||
},
|
},
|
||||||
getGameSystem: async (id: string): Promise<{ status: 200 | 404, json: () => Promise<GameSystem> }> => {
|
getGameSystem: async (
|
||||||
const gs = localStorage.getItem('game-system ' + id);
|
id: string,
|
||||||
if (!gs) return {status: 404, json:async () => ({
|
): Promise<GameSystem> => await idb.read("game-system", id),
|
||||||
accolades: [],
|
};
|
||||||
name: '',
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
schema: emptySchema,
|
|
||||||
schemaId: ''
|
|
||||||
})}
|
|
||||||
return { status: 200, json:async () => (JSON.parse(gs))}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
7
project-warstone/src/services/indexeddb.ts
Normal file
7
project-warstone/src/services/indexeddb.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { IndexedDBService } from "../utils/indexeddb";
|
||||||
|
|
||||||
|
export const idb = new IndexedDBService("commander", 1, [
|
||||||
|
"game-system",
|
||||||
|
"publication",
|
||||||
|
"schema",
|
||||||
|
]);
|
@ -1,4 +1,6 @@
|
|||||||
class IndexedDBService {
|
type Data = Record<string, any> & { storeName: string };
|
||||||
|
|
||||||
|
export class IndexedDBService {
|
||||||
private dbName: string;
|
private dbName: string;
|
||||||
private dbVersion: number;
|
private dbVersion: number;
|
||||||
private storeNames: string[];
|
private storeNames: string[];
|
||||||
@ -17,7 +19,7 @@ class IndexedDBService {
|
|||||||
const request = indexedDB.open(this.dbName, this.dbVersion);
|
const request = indexedDB.open(this.dbName, this.dbVersion);
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
reject(new Error('Failed to open the database.'));
|
reject(new Error("Failed to open the database."));
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
@ -31,13 +33,13 @@ class IndexedDBService {
|
|||||||
// Create all the required object stores during the upgrade
|
// Create all the required object stores during the upgrade
|
||||||
for (const storeName of this.storeNames) {
|
for (const storeName of this.storeNames) {
|
||||||
if (!db.objectStoreNames.contains(storeName)) {
|
if (!db.objectStoreNames.contains(storeName)) {
|
||||||
db.createObjectStore(storeName, { keyPath: 'uuid' });
|
db.createObjectStore(storeName, { keyPath: "uuid" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onblocked = () => {
|
request.onblocked = () => {
|
||||||
reject(new Error('Database is blocked and cannot be accessed.'));
|
reject(new Error("Database is blocked and cannot be accessed."));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -46,11 +48,11 @@ class IndexedDBService {
|
|||||||
return crypto.randomUUID();
|
return crypto.randomUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async create(data: any): Promise<string> {
|
public async create(data: Data): Promise<string> {
|
||||||
const db = await this.openDB();
|
const db = await this.openDB();
|
||||||
const uuid = this.generateUUID();
|
const uuid = this.generateUUID();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = db.transaction(data.storeName, 'readwrite');
|
const transaction = db.transaction(data.storeName, "readwrite");
|
||||||
const objectStore = transaction.objectStore(data.storeName);
|
const objectStore = transaction.objectStore(data.storeName);
|
||||||
|
|
||||||
const request = objectStore.add({ ...data, uuid });
|
const request = objectStore.add({ ...data, uuid });
|
||||||
@ -60,7 +62,7 @@ class IndexedDBService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
reject(new Error('Failed to add data to IndexedDB.'));
|
reject(new Error("Failed to add data to IndexedDB."));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -68,7 +70,7 @@ class IndexedDBService {
|
|||||||
public async read(storeName: string, uuid: string): Promise<any | null> {
|
public async read(storeName: string, uuid: string): Promise<any | null> {
|
||||||
const db = await this.openDB();
|
const db = await this.openDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = db.transaction(storeName, 'readonly');
|
const transaction = db.transaction(storeName, "readonly");
|
||||||
const objectStore = transaction.objectStore(storeName);
|
const objectStore = transaction.objectStore(storeName);
|
||||||
|
|
||||||
const request = objectStore.get(uuid);
|
const request = objectStore.get(uuid);
|
||||||
@ -78,25 +80,29 @@ class IndexedDBService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
reject(new Error('Failed to read data from IndexedDB.'));
|
reject(new Error("Failed to read data from IndexedDB."));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async update(storeName: string, uuid: string, newData: any): Promise<void> {
|
public async update(
|
||||||
|
storeName: string,
|
||||||
|
uuid: string,
|
||||||
|
newData: any,
|
||||||
|
): Promise<void> {
|
||||||
const db = await this.openDB();
|
const db = await this.openDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = db.transaction(storeName, 'readwrite');
|
const transaction = db.transaction(storeName, "readwrite");
|
||||||
const objectStore = transaction.objectStore(storeName);
|
const objectStore = transaction.objectStore(storeName);
|
||||||
|
|
||||||
const request = objectStore.put({ ...newData, uuid }, uuid);
|
const request = objectStore.put({ ...newData, uuid });
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
reject(new Error('Failed to update data in IndexedDB.'));
|
reject(new Error("Failed to update data in IndexedDB."));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -104,7 +110,7 @@ class IndexedDBService {
|
|||||||
public async delete(storeName: string, uuid: string): Promise<void> {
|
public async delete(storeName: string, uuid: string): Promise<void> {
|
||||||
const db = await this.openDB();
|
const db = await this.openDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = db.transaction(storeName, 'readwrite');
|
const transaction = db.transaction(storeName, "readwrite");
|
||||||
const objectStore = transaction.objectStore(storeName);
|
const objectStore = transaction.objectStore(storeName);
|
||||||
|
|
||||||
const request = objectStore.delete(uuid);
|
const request = objectStore.delete(uuid);
|
||||||
@ -114,16 +120,19 @@ class IndexedDBService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
reject(new Error('Failed to delete data from IndexedDB.'));
|
reject(new Error("Failed to delete data from IndexedDB."));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async listAll(storeName: string, fieldName: string): Promise<[string, any][]> {
|
public async listAll<T = any>(
|
||||||
|
storeName: string,
|
||||||
|
fieldName: string,
|
||||||
|
): Promise<[string, T][]> {
|
||||||
const db = await this.openDB();
|
const db = await this.openDB();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = db.transaction(storeName, 'readonly');
|
const transaction = db.transaction(storeName, "readonly");
|
||||||
const objectStore = transaction.objectStore(storeName);
|
const objectStore = transaction.objectStore(storeName);
|
||||||
const request = objectStore.openCursor();
|
const request = objectStore.openCursor();
|
||||||
|
|
||||||
@ -143,14 +152,12 @@ class IndexedDBService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
reject(new Error('Failed to list data from IndexedDB.'));
|
reject(new Error("Failed to list data from IndexedDB."));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createOrUpdate(data: any): Promise<string> {
|
public async createOrUpdate(data: Data): Promise<string> {
|
||||||
const db = await this.openDB();
|
|
||||||
|
|
||||||
// If the provided data already has a UUID, check if it exists in the database
|
// If the provided data already has a UUID, check if it exists in the database
|
||||||
if (data.uuid) {
|
if (data.uuid) {
|
||||||
const existingData = await this.read(data.storeName, data.uuid);
|
const existingData = await this.read(data.storeName, data.uuid);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user