Fixes broken accordion ref

Fixes broken poppable ref
adds Schema page
Fixes schema creation not including game system id
This commit is contained in:
Emmaline Autumn 2024-09-09 08:37:53 -06:00
parent a2fde9cc79
commit c8f20fbda8
9 changed files with 117 additions and 103 deletions

View File

@ -22,6 +22,7 @@ export const saveSchemaDb = async (s: Schema, version: number) => {
}, },
}, },
authorId: sesh.user.id, authorId: sesh.user.id,
gameSystemId: s.gameSystemId,
id: undefined, id: undefined,
}, },
update: { update: {

View File

@ -1,6 +1,6 @@
import { FC, PropsWithChildren } from "react"; import { FC, PropsWithChildren } from "react";
import { Poppable } from "@/lib/poppables/components/poppable"; import { Poppable } from "@/lib/poppables/components/poppable";
import { Icon } from "@/components/Icon"; import { QuestionMarkCircleIcon } from "@heroicons/react/24/solid";
export const HelpPopper: FC<PropsWithChildren> = ({ children }) => { export const HelpPopper: FC<PropsWithChildren> = ({ children }) => {
return ( return (
@ -9,7 +9,7 @@ export const HelpPopper: FC<PropsWithChildren> = ({ children }) => {
preferredAlign="centered" preferredAlign="centered"
preferredEdge="bottom" preferredEdge="bottom"
> >
<Icon icon="Help" className="svg-white w-4 h-4" /> <QuestionMarkCircleIcon className="w-4 h-4 fill-white" />
</Poppable> </Poppable>
); );
}; };

View File

@ -2,13 +2,14 @@ import { FC, useCallback, useEffect, useState } from "react";
import { useObjectStateWrapper } from "../../hooks/useObjectState"; import { useObjectStateWrapper } from "../../hooks/useObjectState";
import { ValueField } from "./value-field"; import { ValueField } from "./value-field";
import { HelpPopper } from "../Poppables/help"; import { HelpPopper } from "../Poppables/help";
import { Icon } from "../Icon";
import { RESERVED_FIELDS } from "../../constants/ReservedFields"; import { RESERVED_FIELDS } from "../../constants/ReservedFields";
import { import {
fieldTypeOptions, fieldTypeOptions,
FieldTypes, FieldTypes,
fieldTypesWithValues, fieldTypesWithValues,
} from "./fieldtypes"; } from "./fieldtypes";
import { TrashIcon } from "@heroicons/react/24/solid";
import { FieldType } from "@/types";
interface IProps { interface IProps {
update: (arg: FieldType) => void; update: (arg: FieldType) => void;
@ -17,9 +18,12 @@ interface IProps {
deleteField: (arg: string) => void; deleteField: (arg: string) => void;
} }
export const FieldEditor: FC<IProps> = ( export const FieldEditor: FC<IProps> = ({
{ update, field, fieldName, deleteField }, update,
) => { field,
fieldName,
deleteField,
}) => {
const { bindProperty, bindPropertyCheck } = useObjectStateWrapper( const { bindProperty, bindPropertyCheck } = useObjectStateWrapper(
field, field,
(e) => update(typeof e === "function" ? e(field) : e), (e) => update(typeof e === "function" ? e(field) : e),
@ -83,9 +87,8 @@ export const FieldEditor: FC<IProps> = (
)} )}
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<label> <label>
<input type="checkbox" {...bindPropertyCheck("isConstant")} /> <input type="checkbox" {...bindPropertyCheck("isConstant")} /> Is
{" "} constant
Is constant
</label> </label>
<HelpPopper> <HelpPopper>
<p className="text-sm"> <p className="text-sm">
@ -108,7 +111,11 @@ export const FieldEditor: FC<IProps> = (
</label> </label>
<label className="w-min"> <label className="w-min">
Limit: Limit:
<input className="w-12 min-w-min" type="number" {...bindProperty("limit")} /> <input
className="w-12 min-w-min"
type="number"
{...bindProperty("limit")}
/>
</label> </label>
<HelpPopper> <HelpPopper>
<p className="text-sm"> <p className="text-sm">
@ -122,11 +129,7 @@ export const FieldEditor: FC<IProps> = (
className="no-default self-end ml-auto" className="no-default self-end ml-auto"
onClick={() => deleteField(fieldName)} onClick={() => deleteField(fieldName)}
> >
<Icon <TrashIcon className="w-6 h-6 fill-white" />
className="svg-red-700 hover:svg-red-500 trash-can w-6 h-6"
icon="Trash"
>
</Icon>
</button> </button>
</div> </div>
)} )}

View File

@ -15,6 +15,8 @@ import { findSchema, saveSchemaDb } from "@/actions/Schemas/index";
import { useToast } from "../toast"; import { useToast } from "../toast";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { Schema, TypeType } from "@/types"; import { Schema, TypeType } from "@/types";
import { TrashIcon } from "@heroicons/react/24/solid";
import { PencilSquareIcon } from "@heroicons/react/24/solid";
export const SchemaBuilder: FC = () => { export const SchemaBuilder: FC = () => {
const [schema, setSchema] = useAtom<Schema>(SchemaEditAtom); const [schema, setSchema] = useAtom<Schema>(SchemaEditAtom);
@ -184,20 +186,14 @@ export const SchemaBuilder: FC = () => {
className="no-default" className="no-default"
onClick={() => selectTypeForEdit(t)} onClick={() => selectTypeForEdit(t)}
> >
<Icon <PencilSquareIcon className="w-6 h-6 fill-white" />
icon="Anvil"
className="anvil svg-olive-drab hover:svg-olive-drab-100 w-6 h-6"
/>
</button> </button>
<button <button
title="Delete" title="Delete"
className="no-default" className="no-default"
onClick={() => deleteType(t)} onClick={() => deleteType(t)}
> >
<Icon <TrashIcon className="w-6 h-6 fill-white" />
icon="Trash"
className="trash-can svg-red-700 hover:svg-red-500 w-6 h-6"
/>
</button> </button>
</div> </div>
</li> </li>

View File

@ -34,10 +34,10 @@ export const SchemaViewer: FC<IProps> = ({ schema, onTypeClick }) => {
<hr /> <hr />
<p className="font-bold italic">Templates</p> <p className="font-bold italic">Templates</p>
<ul> <ul>
{Object.entries(schema.schema).map(([templateKey, template]) => ( {Object.entries(schema.fields).map(([templateKey, template]) => (
<li key={templateKey}> <li key={templateKey}>
<p className="font-bold">{templateKey}</p> <p className="font-bold">{templateKey}</p>
<p className="font-thin text-xs">{template.type}</p> <p className="text-mixed-600 ml-2">Type: {template}</p>
</li> </li>
))} ))}
</ul> </ul>

View File

@ -6,6 +6,7 @@ import { FieldTypes } from "./fieldtypes";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { useInput } from "@/hooks/useInput"; import { useInput } from "@/hooks/useInput";
import { Schema } from "@/types"; import { Schema } from "@/types";
import { TrashIcon } from "@heroicons/react/24/solid";
interface IProps { interface IProps {
templateKey: string; templateKey: string;
@ -19,19 +20,11 @@ export const TemplateEditor: FC<IProps> = ({
fieldType, fieldType,
}) => { }) => {
const [schema, setSchema] = useAtom(SchemaEditAtom); const [schema, setSchema] = useAtom(SchemaEditAtom);
// const updateTemplate = useCallback(
// (t: FieldTypes | ((arg: FieldTypes) => FieldTypes)) => {
// update(templateKey, typeof t === "function" ? t(template) : t);
// },
// [templateKey, update, template],
// );
// const { bindProperty } = useObjectStateWrapper(template, updateTemplate);
const { bind: bindFieldType, value } = useInput(fieldType); const { bind: bindFieldType, value } = useInput(fieldType);
useEffect(() => { useEffect(() => {
update(templateKey, value); update(templateKey, value);
}); }, []);
const deleteField = useCallback(() => { const deleteField = useCallback(() => {
setSchema((s: Schema) => { setSchema((s: Schema) => {
@ -70,10 +63,7 @@ export const TemplateEditor: FC<IProps> = ({
</label> </label>
</div> </div>
<button className="no-default" onClick={deleteField}> <button className="no-default" onClick={deleteField}>
<Icon <TrashIcon className="w-6 h-6 fill-white" />
icon="Trash"
className="svg-red-700 hover:svg-red-500 trash-can w-6 h-6"
/>
</button> </button>
</div> </div>
</li> </li>

View File

@ -9,6 +9,7 @@ import { useObjectState } from "../../hooks/useObjectState";
import { useInput } from "../../hooks/useInput"; import { useInput } from "../../hooks/useInput";
import { FieldEditor } from "./field-editor"; import { FieldEditor } from "./field-editor";
import { FieldTypes } from "./fieldtypes"; import { FieldTypes } from "./fieldtypes";
import { FieldType, TypeType } from "@/types";
interface IProps { interface IProps {
name: string; name: string;
@ -18,9 +19,11 @@ interface IProps {
const constantProperties = ["metadata"]; const constantProperties = ["metadata"];
export const TypeEditor: FC<PropsWithChildren<IProps>> = ( export const TypeEditor: FC<PropsWithChildren<IProps>> = ({
{ saveType, name, type: passedType }, saveType,
) => { name,
type: passedType,
}) => {
const { const {
update: updateType, update: updateType,
reset: resetType, reset: resetType,
@ -39,7 +42,8 @@ export const TypeEditor: FC<PropsWithChildren<IProps>> = (
resetType(); resetType();
}; };
const addField = useCallback((e: FormEvent) => { const addField = useCallback(
(e: FormEvent) => {
e.preventDefault(); e.preventDefault();
updateType({ updateType({
[propertyName]: { [propertyName]: {
@ -51,7 +55,9 @@ export const TypeEditor: FC<PropsWithChildren<IProps>> = (
}, },
}); });
resetPropertyName(); resetPropertyName();
}, [propertyName, updateType, resetPropertyName]); },
[propertyName, updateType, resetPropertyName],
);
const updateField = useCallback( const updateField = useCallback(
(k: keyof typeof type) => (field: FieldType) => { (k: keyof typeof type) => (field: FieldType) => {
@ -64,13 +70,16 @@ export const TypeEditor: FC<PropsWithChildren<IProps>> = (
passedType && setType(passedType); passedType && setType(passedType);
}, [passedType, setType]); }, [passedType, setType]);
const deleteField = useCallback((name: string) => { const deleteField = useCallback(
(name: string) => {
setType((t) => { setType((t) => {
const fields = { ...t }; const fields = { ...t };
delete fields[name]; delete fields[name];
return fields; return fields;
}); });
}, [setType]); },
[setType],
);
return ( return (
<div> <div>
@ -82,9 +91,10 @@ export const TypeEditor: FC<PropsWithChildren<IProps>> = (
<button disabled={!propertyName}>Add Field</button> <button disabled={!propertyName}>Add Field</button>
</form> </form>
<ul className="rounded-lg overflow-hidden"> <ul className="rounded-lg overflow-hidden">
{Object.entries(type).reverse().filter(([k]) => {Object.entries(type)
!constantProperties.includes(k) .reverse()
).map(([key, value]) => ( .filter(([k]) => !constantProperties.includes(k))
.map(([key, value]) => (
<FieldEditor <FieldEditor
key={"field-editor" + key} key={"field-editor" + key}
field={value} field={value}

View File

@ -6,17 +6,21 @@ interface IProps {
title?: ReactNode; title?: ReactNode;
} }
export const Accordion: FC<PropsWithChildren<IProps>> = ( export const Accordion: FC<PropsWithChildren<IProps>> = ({
{ children, expandOnHover, expanded, title }, children,
) => { expandOnHover,
expanded,
title,
}) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
<div <div
data-expanded={open || expanded} data-expanded={open || expanded}
data-expandonhover={expandOnHover} data-expandonhover={expandOnHover}
className={(expandOnHover ? "group/hover" : "group/controlled") + className={
" group"} (expandOnHover ? "group/hover" : "group/controlled") + " group"
}
onClick={() => !title && !expandOnHover && setOpen(!open)} onClick={() => !title && !expandOnHover && setOpen(!open)}
> >
{!!title && ( {!!title && (
@ -24,9 +28,7 @@ export const Accordion: FC<PropsWithChildren<IProps>> = (
className="flex justify-between cursor-pointer" className="flex justify-between cursor-pointer"
onClick={() => !expandOnHover && setOpen(!open)} onClick={() => !expandOnHover && setOpen(!open)}
> >
<div className="accordion-title"> <div className="accordion-title">{title}</div>
{title}
</div>
<div <div
className={` className={`
group-hover/hover:-rotate-180 group-hover/hover:-rotate-180
@ -41,10 +43,8 @@ export const Accordion: FC<PropsWithChildren<IProps>> = (
scale-y-50 scale-y-50
`} `}
> >
<span className="block w-2 h-2 rotate-45 border-r-2 border-b-2 place-self-center"> <span className="block w-2 h-2 rotate-45 border-r-2 border-b-2 place-self-center"></span>
</span> <span className="block w-2 h-2 rotate-45 border-r-2 border-b-2 place-self-center"></span>
<span className="block w-2 h-2 rotate-45 border-r-2 border-b-2 place-self-center">
</span>
</div> </div>
</div> </div>
)} )}
@ -64,15 +64,15 @@ export const AccordionContent: FC<PropsWithChildren> = ({ children }) => {
} }
}, []); }, []);
const Child = () => (
<div className="absolute bottom-0 w-full" ref={updateRef}>
{children}
</div>
);
return ( return (
<div className="relative overflow-hidden"> <div className="relative overflow-hidden">
{<Child />} <div
key={"accordion-content"}
className="absolute bottom-0 w-full"
ref={updateRef}
>
{children}
</div>
<span <span
style={{ ["--v-height" as never]: height + "px" }} style={{ ["--v-height" as never]: height + "px" }}
className="w-0 block h-0 group-hover/hover:h-variable group-data-[expanded]/controlled:h-variable transition-all duration-700" className="w-0 block h-0 group-hover/hover:h-variable group-data-[expanded]/controlled:h-variable transition-all duration-700"

View File

@ -1,6 +1,13 @@
"use client"; "use client";
import { FC, PropsWithChildren, ReactNode, useCallback, useState } from "react"; import {
FC,
PropsWithChildren,
ReactNode,
useCallback,
useRef,
useState,
} from "react";
import { PoppableContent } from "./poppable-content"; import { PoppableContent } from "./poppable-content";
import { useDebounce } from "../../../hooks/useDebounce"; import { useDebounce } from "../../../hooks/useDebounce";
@ -12,38 +19,45 @@ interface IProps {
spacing?: number; spacing?: number;
} }
export const Poppable: FC<PropsWithChildren<IProps>> = ( export const Poppable: FC<PropsWithChildren<IProps>> = ({
{ className, content, children, preferredEdge, preferredAlign, spacing }, className,
) => { content,
children,
preferredEdge,
preferredAlign,
spacing,
}) => {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const closing = useDebounce(!isHovered, 1000); const closing = useDebounce(!isHovered, 1000);
const closed = useDebounce(closing, 300); const closed = useDebounce(closing, 300);
const [ref, setRef] = useState<HTMLElement>(); // const [ref, setRef] = useState<HTMLElement>();
const updateRef = useCallback((node: HTMLElement) => { // const updateRef = useCallback((node: HTMLElement) => {
if (!node) return; // if (!node) return;
setRef(node); // setRef(node);
}, []); // }, []);
const ref = useRef(null);
return ( return (
<> <>
<span <span
ref={updateRef} ref={ref}
className={className} className={className}
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
> >
{children} {children}
</span> </span>
{!!ref && ( {!!ref.current && (
<PoppableContent <PoppableContent
preferredAlign={preferredAlign} preferredAlign={preferredAlign}
preferredEdge={preferredEdge} preferredEdge={preferredEdge}
spacing={spacing} spacing={spacing}
isClosing={closing} isClosing={closing}
isClosed={closed} isClosed={closed}
relativeElement={ref} relativeElement={ref.current}
setHover={setIsHovered} setHover={setIsHovered}
> >
{content} {content}