diff --git a/project-warstone/bun.lockb b/project-warstone/bun.lockb new file mode 100755 index 0000000..6da180d Binary files /dev/null and b/project-warstone/bun.lockb differ diff --git a/project-warstone/package.json b/project-warstone/package.json index 9f9289e..329a745 100644 --- a/project-warstone/package.json +++ b/project-warstone/package.json @@ -17,7 +17,7 @@ "postcss": "^8.4.24", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.12.0", + "react-router-dom": "^6.21.0", "recoil": "^0.7.7", "recoilize": "^3.2.0", "tailwindcss": "^3.3.2", diff --git a/project-warstone/query syntax b/project-warstone/query syntax.txt similarity index 86% rename from project-warstone/query syntax rename to project-warstone/query syntax.txt index 2ca7f02..88e5087 100644 --- a/project-warstone/query syntax +++ b/project-warstone/query syntax.txt @@ -1,9 +1,11 @@ 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: ^ - refers specifically to the object based off of this specific type Query relative object: $ - - refers to the last relative object in the heirarchy + - refers to the last relative object in the hierarchy Access child: . - 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 @@ -45,6 +47,8 @@ separator: ::;; Examples: ?core.weapon_abilities[name=Rapid Fire].body - 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}} - 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}}< + + + + + {/* */} + + {/* */} + + - - {/* */} ) } diff --git a/project-warstone/src/components/GameSystemEditor/index.tsx b/project-warstone/src/components/GameSystemEditor/index.tsx index 995f57d..771b0cf 100644 --- a/project-warstone/src/components/GameSystemEditor/index.tsx +++ b/project-warstone/src/components/GameSystemEditor/index.tsx @@ -16,19 +16,20 @@ export const GameSystemEditor: FC = () => { name: 'Asshammer 40x a day', accolades: [], }); - const [schemas, setSchemas] = useState<{ name: string, id: string }[]>([]); + const [schemas, setSchemas] = useState<[string,string][]>([]); const [lastSaved, setLastSaved] = useState(gameSystem); const fetchSchema = useCallback(async (id: string) => { - const res = await GameSystemsService.getSchema(id); + try { + const schema = await GameSystemsService.getSchema(id); - if (res.status !== 200) return; - const schema = await res.json() - - updateGameSystem({ - schema - }); + updateGameSystem({ + schema + }); + } catch (e) { + console.log('failed to fetch schema:', e) + } }, [updateGameSystem]) useEffect(() => { @@ -38,7 +39,6 @@ export const GameSystemEditor: FC = () => { useEffect(() => { GameSystemsService.getSchemaList() - .then(res => res.json()) .then(schemas => setSchemas(schemas)); }, []); @@ -48,8 +48,7 @@ export const GameSystemEditor: FC = () => { }, [gameSystem]) const fetchGameSystem = useCallback(async () => { - const res = await GameSystemsService.getGameSystem(''); - const gs = await res.json(); + const gs = await GameSystemsService.getGameSystem(''); setGameSystem(gs); setLastSaved(gs); }, [setGameSystem]); @@ -66,7 +65,7 @@ export const GameSystemEditor: FC = () => { diff --git a/project-warstone/src/components/SchemaBuilder/field-editor.tsx b/project-warstone/src/components/SchemaBuilder/field-editor.tsx index b57387c..82477c5 100644 --- a/project-warstone/src/components/SchemaBuilder/field-editor.tsx +++ b/project-warstone/src/components/SchemaBuilder/field-editor.tsx @@ -24,9 +24,9 @@ export const FieldEditor: FC = ({ update, field, fieldName, deleteField setReserved(RESERVED_FIELDS[fieldName]); }, [fieldName]) - useEffect(() => { - console.log(field.value); - }, [field]) + // useEffect(() => { + // console.log(field.value); + // }, [field]) return (
  • diff --git a/project-warstone/src/components/SchemaBuilder/index.tsx b/project-warstone/src/components/SchemaBuilder/index.tsx index 829a7bf..c063382 100644 --- a/project-warstone/src/components/SchemaBuilder/index.tsx +++ b/project-warstone/src/components/SchemaBuilder/index.tsx @@ -4,17 +4,23 @@ import { TypeEditor } from './type-editor'; import { useObjectState, useObjectStateWrapper } from '../../hooks/useObjectState'; import { FieldTypes, Schema, Template, TypeType } from '../../types/schema'; import { useInput } from '../../hooks/useInput'; -import { useRecoilState } from 'recoil'; +import { useRecoilState, useResetRecoilState } from 'recoil'; import { SchemaEditAtom } from '../../recoil/atoms/schema'; import { GameSystemsService } from '../../services/game-systems'; import { SchemaViewer } from './schema-viewer'; import { TemplateEditor } from './template-editor'; import { Icon } from '../Icon'; +import { useNavigate, useParams } from 'react-router-dom'; export const SchemaBuilder: FC = () => { const [schema, setSchema] = useRecoilState(SchemaEditAtom); - const { update: updateSchema } = useObjectStateWrapper(schema, setSchema); + const resetSchema = useResetRecoilState(SchemaEditAtom); + const { update: updateSchema, bindProperty:bindSchemaProperty } = useObjectStateWrapper(schema, setSchema); + + const navigate = useNavigate(); + + const {id} = useParams<{id: string}>() const { value: typeName, bind: bindTypeName, reset: resetTypeName } = useInput(''); @@ -23,19 +29,18 @@ export const SchemaBuilder: FC = () => { const [lastSaved, setLastSaved] = useState(schema); const fetchSchema = useCallback(async () => { - const result = await GameSystemsService.getSchema('286f4c18-d280-444b-8d7e-9a3dd09f64ef') - if (result.status !== 200) return; - const fetchedSchema = await result.json(); + if (!id) return; + if (id === 'new') return resetSchema(); + const fetchedSchema = await GameSystemsService.getSchema(id) // if (fetchedSchema.name === schema.name) return; if (!fetchedSchema.templates) fetchedSchema.templates = {} setSchema(fetchedSchema); setLastSaved(fetchedSchema); - }, [setSchema]) + }, []) useEffect(() => { fetchSchema(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [fetchSchema]) const [selectedType, setSelectedType] = useState(''); @@ -51,9 +56,10 @@ export const SchemaBuilder: FC = () => { setSelectedType(''); }, [resetTypeName, updateSchema]); - const saveSchema = useCallback(() => { - GameSystemsService.saveSchema(schema); + const saveSchema = useCallback(async () => { setLastSaved(schema); + const sid = await GameSystemsService.saveSchema(schema); + if (id === 'new') navigate('/schema/'+sid) }, [schema]) const selectTypeForEdit = useCallback((typeKey: string) => { @@ -95,6 +101,9 @@ export const SchemaBuilder: FC = () => { return (
    +
    + +

    Add a template

    @@ -103,7 +112,7 @@ export const SchemaBuilder: FC = () => {
      {Object.entries(schema.templates).map(([templateKey, template]) => ( - + ))}
    @@ -119,7 +128,7 @@ export const SchemaBuilder: FC = () => {
      {Object.keys(schema.types).map(t => ( -
    • +
    • {t}
      +
        + {schemas.map(([id,name])=>
      • {name || 'Unnamed Schema'}
      • )} +
      +
      + ) +} \ No newline at end of file diff --git a/project-warstone/src/constants/TemplateTypes.ts b/project-warstone/src/constants/TemplateTypes.ts index 610ec82..0e0fb25 100644 --- a/project-warstone/src/constants/TemplateTypes.ts +++ b/project-warstone/src/constants/TemplateTypes.ts @@ -1,4 +1,4 @@ -import { FieldTypes, TypeType } from '../types/schema'; +import { FieldTypes, TypeType } from "../types/schema"; export const TEMPLATE_TYPES: Record = { section: { @@ -7,14 +7,14 @@ export const TEMPLATE_TYPES: Record = { limit: 1, minimum: 1, type: FieldTypes.text, - value: '' + value: "", }, body: { isConstant: false, limit: 0, minimum: 1, - type: FieldTypes['long text'], - value: '' + type: FieldTypes["long text"], + value: "", }, }, steps: { @@ -23,8 +23,8 @@ export const TEMPLATE_TYPES: Record = { limit: 0, minimum: 1, type: FieldTypes.type, - value: 'section' - } + value: "section", + }, }, image: { name: { @@ -32,14 +32,14 @@ export const TEMPLATE_TYPES: Record = { limit: 1, minimum: 1, type: FieldTypes.text, - value: '' + value: "", }, link: { isConstant: false, limit: 1, minimum: 1, type: FieldTypes.text, - value: '' + value: "", }, }, list: { @@ -47,18 +47,34 @@ export const TEMPLATE_TYPES: Record = { isConstant: false, limit: 0, minimum: 1, - type: FieldTypes['long text'], - value: '' - } + type: FieldTypes["long text"], + 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: { columns: { isConstant: false, limit: 0, minimum: 1, - type: FieldTypes.any, - value: '' - } + type: FieldTypes.type, + value: "tableColumn", + }, }, table: { rows: { @@ -66,14 +82,14 @@ export const TEMPLATE_TYPES: Record = { limit: 0, minimum: 1, type: FieldTypes.type, - value: 'tableRow' + value: "tableRow", }, header: { isConstant: false, limit: 1, minimum: 0, type: FieldTypes.type, - value: 'tableRow' - } - } -}; \ No newline at end of file + value: "tableRow", + }, + }, +}; diff --git a/project-warstone/src/hooks/useObjectState.ts b/project-warstone/src/hooks/useObjectState.ts index fcea972..08363cd 100644 --- a/project-warstone/src/hooks/useObjectState.ts +++ b/project-warstone/src/hooks/useObjectState.ts @@ -1,46 +1,58 @@ -import React, { ChangeEvent, useCallback, useState } from 'react'; -import { InputBinder } from '../types/inputBinder'; +import React, { ChangeEvent, useCallback, useState } from "react"; +import { InputBinder } from "../types/inputBinder"; type ObjectStateHookConfig = { disallowSpaces?: boolean; spaceReplacer?: string; -} +}; export const useObjectState = (initial: T) => { const [state, setState] = useState(initial || {} as T); - const bindProperty = useCallback((property: K, config: ObjectStateHookConfig) => ({ - value: state[property] ?? '', - name: property, - onChange: (event: ChangeEvent) => - setState(value => ( - { - ...value, - [event.target.name]: ( - (typeof value[property] === 'number') ? - Number(event.target.value) || 0 : - config?.disallowSpaces ? - event.target.value.replace(' ', config.spaceReplacer || '_') : - event.target.value - ) - } - )) - }), [state]) + const bindProperty = useCallback( + (property: K, config?: ObjectStateHookConfig) => ({ + value: state[property] ?? "", + name: property, + onChange: ( + event: ChangeEvent< + HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement + >, + ) => + setState((value) => ( + { + ...value, + [event.target.name]: ( + (typeof value[property] === "number") + ? Number(event.target.value) || 0 + : config?.disallowSpaces + ? event.target.value.replace(" ", config.spaceReplacer || "_") + : event.target.value + ), + } + )), + }), + [state], + ); const bindPropertyCheck = useCallback((property: K) => ({ checked: !!state[property], name: property, - onChange: (event: ChangeEvent) => setState(value => ({ - ...value, [event.target.name]: (event.target.checked) - })), - readOnly: true - }), [state]) + onChange: (event: ChangeEvent) => + setState((value) => ({ + ...value, + [event.target.name]: (event.target.checked), + })), + readOnly: true, + }), [state]); - const update = useCallback((updates: Partial) => setState(s => ({ ...s, ...updates })), []) + const update = useCallback( + (updates: Partial) => setState((s) => ({ ...s, ...updates })), + [], + ); const reset = useCallback(() => { setState(initial); - }, [initial]) + }, [initial]); return { bindProperty, @@ -48,48 +60,67 @@ export const useObjectState = (initial: T) => { update, state, setState, - reset - } -} + reset, + }; +}; -export const useObjectStateWrapper = (state: T, setState: React.Dispatch>) => { - - const bindProperty = useCallback((property: K, config?: ObjectStateHookConfig): InputBinder => ({ - value: state[property] ?? '', - name: property.toString(), - onChange: (event: ChangeEvent) => - setState(value => ( - { - ...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]) +export const useObjectStateWrapper = ( + state: T, + setState: React.Dispatch>, +) => { + const bindProperty = useCallback( + ( + property: K, + config?: ObjectStateHookConfig, + ): InputBinder => ({ + value: state[property]?.toString() ?? "", + name: property.toString(), + onChange: ( + event: ChangeEvent< + HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement + >, + ) => + setState((value) => ( + { + ...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((property: K) => ({ checked: !!state[property], name: property, - onChange: (event: ChangeEvent) => setState(value => ({ - ...value, [event.target.name]: (event.target.checked) - })), - readOnly: true - }), [setState, state]) + onChange: (event: ChangeEvent) => + setState((value) => ({ + ...value, + [event.target.name]: (event.target.checked), + })), + readOnly: true, + }), [setState, state]); - const update = useCallback((updates: Partial | ((arg: T) => Partial)) => - setState(s => ({ ...s, ...(typeof updates === 'function' ? updates(s) : updates) } - )), [setState]) + const update = useCallback( + (updates: Partial | ((arg: T) => Partial)) => + setState((s) => ({ + ...s, + ...(typeof updates === "function" ? updates(s) : updates), + })), + [setState], + ); return { bindProperty, bindPropertyCheck, update, state, - setState - } -} \ No newline at end of file + setState, + }; +}; diff --git a/project-warstone/src/lib/accordion/index.tsx b/project-warstone/src/lib/accordion/index.tsx index 8925b57..893fb7e 100644 --- a/project-warstone/src/lib/accordion/index.tsx +++ b/project-warstone/src/lib/accordion/index.tsx @@ -13,7 +13,7 @@ export const Accordion: FCC = ({ children, expandOnHover, expanded, titl return (
      !title && !expandOnHover && setOpen(!open)} > diff --git a/project-warstone/src/services/game-systems.ts b/project-warstone/src/services/game-systems.ts index ec95e1d..131d08c 100644 --- a/project-warstone/src/services/game-systems.ts +++ b/project-warstone/src/services/game-systems.ts @@ -1,51 +1,45 @@ -import { GameSystem } from '../types/gameSystem'; -import { Schema } from '../types/schema'; +import { GameSystem } from "../types/gameSystem"; +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 = { // todo - connect to service to save schema for game saveSchema: async (schema: Schema) => { - localStorage.setItem('schema ' + schema.id, JSON.stringify(schema)); - - return { status: 200 } + // localStorage.setItem('schema ' + schema.id, JSON.stringify(schema)); + try { + return await idb.createOrUpdate({ + storeName: "schema", + ...schema, + }); + } catch (e) { + console.log(e); + } }, // todo - connect to service to fetch schema for game - getSchema: async (id: string): Promise<{ status: 200 | 404, json: () => Promise }> => { - const schema = localStorage.getItem('schema ' + id); - - if (schema) - return { - status: 200, - json: async () => JSON.parse(schema) - } - - return { - status: 404, - json: async () => (emptySchema) - } - }, - getSchemaList: async () => { - return { - status: 200, json: async () => [{ - name: 'Test Schema', - id: '286f4c18-d280-444b-8d7e-9a3dd09f64ef' - }] + getSchema: async ( + id: string, + ): Promise => { + try { + const schema = await idb.read("schema", id); + return schema; + } catch (e) { + throw e; } }, + getSchemaList: async () => await idb.listAll("schema", "name"), + saveGameSystem: async (gs: GameSystem) => { - localStorage.setItem('game-system ' + gs.id, JSON.stringify(gs)); - return { status: 200 } + localStorage.setItem("game-system " + gs.id, JSON.stringify(gs)); + return { status: 200 }; }, - getGameSystem: async (id: string): Promise<{ status: 200 | 404, json: () => Promise }> => { - const gs = localStorage.getItem('game-system ' + id); - if (!gs) return {status: 404, json:async () => ({ - accolades: [], - name: '', - id: crypto.randomUUID(), - schema: emptySchema, - schemaId: '' - })} - return { status: 200, json:async () => (JSON.parse(gs))} - } -} \ No newline at end of file + getGameSystem: async ( + id: string, + ): Promise => await idb.read("game-system", id), +}; diff --git a/project-warstone/src/services/indexeddb.ts b/project-warstone/src/services/indexeddb.ts new file mode 100644 index 0000000..b8eb002 --- /dev/null +++ b/project-warstone/src/services/indexeddb.ts @@ -0,0 +1,7 @@ +import { IndexedDBService } from "../utils/indexeddb"; + +export const idb = new IndexedDBService("commander", 1, [ + "game-system", + "publication", + "schema", +]); diff --git a/project-warstone/src/utils/indexeddb.ts b/project-warstone/src/utils/indexeddb.ts index 419023c..3406e81 100644 --- a/project-warstone/src/utils/indexeddb.ts +++ b/project-warstone/src/utils/indexeddb.ts @@ -1,4 +1,6 @@ -class IndexedDBService { +type Data = Record & { storeName: string }; + +export class IndexedDBService { private dbName: string; private dbVersion: number; private storeNames: string[]; @@ -17,7 +19,7 @@ class IndexedDBService { const request = indexedDB.open(this.dbName, this.dbVersion); request.onerror = () => { - reject(new Error('Failed to open the database.')); + reject(new Error("Failed to open the database.")); }; request.onsuccess = () => { @@ -31,13 +33,13 @@ class IndexedDBService { // Create all the required object stores during the upgrade for (const storeName of this.storeNames) { if (!db.objectStoreNames.contains(storeName)) { - db.createObjectStore(storeName, { keyPath: 'uuid' }); + db.createObjectStore(storeName, { keyPath: "uuid" }); } } }; 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(); } - public async create(data: any): Promise { + public async create(data: Data): Promise { const db = await this.openDB(); const uuid = this.generateUUID(); 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 request = objectStore.add({ ...data, uuid }); @@ -60,7 +62,7 @@ class IndexedDBService { }; 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 { const db = await this.openDB(); return new Promise((resolve, reject) => { - const transaction = db.transaction(storeName, 'readonly'); + const transaction = db.transaction(storeName, "readonly"); const objectStore = transaction.objectStore(storeName); const request = objectStore.get(uuid); @@ -78,25 +80,29 @@ class IndexedDBService { }; 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 { + public async update( + storeName: string, + uuid: string, + newData: any, + ): Promise { const db = await this.openDB(); return new Promise((resolve, reject) => { - const transaction = db.transaction(storeName, 'readwrite'); + const transaction = db.transaction(storeName, "readwrite"); const objectStore = transaction.objectStore(storeName); - const request = objectStore.put({ ...newData, uuid }, uuid); + const request = objectStore.put({ ...newData, uuid }); request.onsuccess = () => { resolve(); }; 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 { const db = await this.openDB(); return new Promise((resolve, reject) => { - const transaction = db.transaction(storeName, 'readwrite'); + const transaction = db.transaction(storeName, "readwrite"); const objectStore = transaction.objectStore(storeName); const request = objectStore.delete(uuid); @@ -114,16 +120,19 @@ class IndexedDBService { }; 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( + storeName: string, + fieldName: string, + ): Promise<[string, T][]> { const db = await this.openDB(); return new Promise((resolve, reject) => { - const transaction = db.transaction(storeName, 'readonly'); + const transaction = db.transaction(storeName, "readonly"); const objectStore = transaction.objectStore(storeName); const request = objectStore.openCursor(); @@ -143,14 +152,12 @@ class IndexedDBService { }; 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 { - const db = await this.openDB(); - + public async createOrUpdate(data: Data): Promise { // If the provided data already has a UUID, check if it exists in the database if (data.uuid) { const existingData = await this.read(data.storeName, data.uuid);