diff --git a/.gitignore b/.gitignore index 1ddcf91..8317c16 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ .idea/ /vendor/ +dist +node_modules +.cache +composer.phar +*.zip diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..222861c --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "useTabs": false +} diff --git a/.wp-env.json b/.wp-env.json new file mode 100644 index 0000000..1f9c21c --- /dev/null +++ b/.wp-env.json @@ -0,0 +1,6 @@ +{ + "plugins": ["."], + "config": { + "WP_DEBUG_DISPLAY": true + } +} \ No newline at end of file diff --git a/assets/icon-128x128.png b/assets/icon-128x128.png old mode 100755 new mode 100644 diff --git a/assets/icon-256x256.png b/assets/icon-256x256.png old mode 100755 new mode 100644 diff --git a/assets/icon-500x500.png b/assets/icon-500x500.png old mode 100755 new mode 100644 diff --git a/assets/lib/CustomizerUI.tsx b/assets/lib/CustomizerUI.tsx new file mode 100644 index 0000000..c075c99 --- /dev/null +++ b/assets/lib/CustomizerUI.tsx @@ -0,0 +1,69 @@ +import { Component } from "react"; +import React = require("react"); +import WarningBar from "./elements/WarningBar"; +import { upgrade } from "./services/database"; +import { fetchData } from "./services/api"; +import CustomizerEditor from "./components/CustomizerEditor"; +import TabPane from "./components/TabPane"; +import { connect } from "react-redux"; +import store, { actions } from "./redux/wpcuiReducer"; +import Notification from "./components/Notification"; +import Modal from "./components/Modal"; +import { DatabaseObject, NavigationTab } from "./models/models"; +import { getNavigationTabs } from "./services/navigation"; + +interface IProps { + data: DatabaseObject; +} +class CustomizerUI extends Component { + constructor(props) { + super(props); + + this.upgradeDatabase = this.upgradeDatabase.bind(this); + } + + componentDidMount() { + fetchData().then((data) => { + store.dispatch({ + type: actions.DATA_FETCH, + data, + }); + }); + } + + upgradeDatabase() { + upgrade(this.props.data); + } + + databaseUpgradeWarning() { + return ( + + ); + } + + render() { + if (this.props.data.db_version < 2 && this.props.data.db_version > 0) { + return this.databaseUpgradeWarning(); + } else if (this.props.data.sections) { + return ( +
+ + + +
+ ); + } else { + return

Loading ...

; + } + } +} + +const mapStateToProps = (state) => ({ + data: state, +}); +export default connect(mapStateToProps)(CustomizerUI); diff --git a/assets/lib/common.ts b/assets/lib/common.ts new file mode 100644 index 0000000..828275d --- /dev/null +++ b/assets/lib/common.ts @@ -0,0 +1,59 @@ +import { Control, DatabaseObject, Settings } from "./models/models"; + +export function stringToSnakeCase(input: string): string { + const strArr = input.split(" "); + const snakeArr = strArr.reduce((acc, val) => { + return acc.concat(val.toLowerCase()); + }, []); + return snakeArr.join("_"); +} + +/** + * Check across all controls in every section to see + * if the given control ID already exists. + * @param controlId + * @param data + */ +export function controlIdExists( + controlId: string, + data: DatabaseObject +): boolean { + let exists = false; + + data.sections.forEach((section) => { + section.controls.forEach((control) => { + if (control.id === controlId) exists = true; + }); + }); + + return exists; +} + +/** + * Check across all sections to see if the given section ID + * is already in use. + * @param sectionId + * @param data + */ +export function sectionIdExists( + sectionId: string, + data: DatabaseObject +): boolean { + let exists = false; + + data.sections.forEach((section) => { + if (section.id === sectionId) exists = true; + }); + + return exists; +} + + +/** + * Get the full control ID, including the prefix (if any). + * @param control + * @param data + */ +export function getFullControlId(control: Control, settings: Settings) { + return settings.controlPrefix ? `${settings.controlPrefix}_${control.id}` : control.id; +} \ No newline at end of file diff --git a/assets/lib/components/CardHeader.tsx b/assets/lib/components/CardHeader.tsx new file mode 100644 index 0000000..d8bac9e --- /dev/null +++ b/assets/lib/components/CardHeader.tsx @@ -0,0 +1,73 @@ +import { Component } from "react"; +import React = require("react"); +import { CardHeaderBar } from "../styled"; + +interface IconButton { + function: Function; + title: string; +} + +interface IProps { + onDuplicate?: IconButton; + onEdit?: IconButton; + onDelete?: IconButton; + onCode?: IconButton; + title?: string; +} + +export default class CardHeader extends Component { + renderHeaderButtons() { + const buttons = []; + if (this.props.onCode) { + buttons.push( + this.props.onCode.function()} + className="dashicons dashicons-editor-code" + /> + ); + } + if (this.props.onDuplicate) { + buttons.push( + this.props.onDuplicate.function} + className="dashicons dashicons-admin-page" + /> + ); + } + if (this.props.onEdit) { + buttons.push( + this.props.onEdit.function()} + className="dashicons dashicons-edit" + /> + ); + } + if (this.props.onDelete) { + buttons.push( + this.props.onDelete.function()} + className="dashicons dashicons-trash" + /> + ); + } + + return buttons; + } + + render() { + return ( + +
{this.props.title}
+
{this.renderHeaderButtons()}
+
+ ); + } +} diff --git a/assets/lib/components/Control.tsx b/assets/lib/components/Control.tsx new file mode 100644 index 0000000..1fbf541 --- /dev/null +++ b/assets/lib/components/Control.tsx @@ -0,0 +1,89 @@ +import { Component } from "react"; +import { Control as CustomizerControl, Settings } from "../models/models"; +import React = require("react"); +import store, { actions } from "../redux/wpcuiReducer"; +import CardHeader from "./CardHeader"; +import { Card, CardContents } from "../styled"; +import { GetControlTypeById } from "../models/selectOptions"; +import { hideModal, modal } from "./Modal"; +import ControlForm from "../forms/ControlForm"; +import { ModalWrapper, ModalContent, CodeSample, ButtonBar } from "../styled"; +import Button from "../elements/Button"; +import { getFullControlId } from "../common"; + +interface IProps { + control: CustomizerControl; + settings: Settings; + prefix: string; +} + +export default class Control extends Component { + constructor(props: IProps) { + super(props); + + this.delete = this.delete.bind(this); + this.edit = this.edit.bind(this); + this.showCode = this.showCode.bind(this); + } + + delete() { + let res = confirm( + `Are you sure that you want to delete the control with ID of ${this.props.control.id}` + ); + + if (res) { + store.dispatch({ + type: actions.DELETE_CONTROL, + controlId: this.props.control.id + }); + } + } + + copyCode() { + // @ts-ignore + document.getElementById('wpcui_sample_code').select(); + document.execCommand('copy'); + alert('Code copied to your clipboard!'); + } + + showCode() { + const id = this.props.prefix ? `${this.props.prefix}_${this.props.control.id}` : this.props.control.id; + const sample = `get_theme_mod( '${id}', '${this.props.control.default ? this.props.control.default : "Default Value"}' )`; + modal( + + + + + this.copyCode()} title="Copy code" className="wpcui-copy-icon dashicons dashicons-admin-page"> + + + + + ); + } +} diff --git a/assets/lib/elements/FormCancel.tsx b/assets/lib/elements/FormCancel.tsx new file mode 100644 index 0000000..902f864 --- /dev/null +++ b/assets/lib/elements/FormCancel.tsx @@ -0,0 +1,21 @@ +import { Component, MouseEventHandler } from "react"; +import React = require("react"); +import { FormCancelButton } from "../styled"; + +interface IProps { + handleClick: MouseEventHandler; +} +interface IState {} + +export default class FormCancel extends Component { + render() { + return ( + + [Cancel] + + ); + } +} diff --git a/assets/lib/elements/FormCheckbox.tsx b/assets/lib/elements/FormCheckbox.tsx new file mode 100644 index 0000000..ed3b9be --- /dev/null +++ b/assets/lib/elements/FormCheckbox.tsx @@ -0,0 +1,25 @@ +import { ChangeEventHandler, Component } from "react"; +import React = require("react"); + +interface IProps { + label: string; + checked?: boolean; + handleChange: ChangeEventHandler; +} + +export default class FormCheckbox extends Component { + render() { + return ( + + {this.props.label} + + + + + ); + } +} diff --git a/assets/lib/elements/FormSelect.tsx b/assets/lib/elements/FormSelect.tsx new file mode 100644 index 0000000..b233bd6 --- /dev/null +++ b/assets/lib/elements/FormSelect.tsx @@ -0,0 +1,56 @@ +import { ChangeEventHandler, Component } from "react"; +import React = require("react"); +import { SelectOption } from "../models/selectOptions"; + +interface IProps { + inputId: string; + label: string; + onChange: ChangeEventHandler; + value?: number; + options: SelectOption[]; +} +interface IState {} + +export default class FormSelect extends Component { + constructor(props) { + super(props); + + this.renderLabel = this.renderLabel.bind(this); + this.renderInput = this.renderInput.bind(this); + } + + renderLabel() { + return this.props.label ? ( + + ) : ( + "" + ); + } + + renderInput() { + return ( + + ); + } + + getOptionItems() { + let items = []; + this.props.options.forEach((option) => + items.push( + + ) + ); + return items; + } + + render() { + return ( + + {this.renderLabel()} + {this.renderInput()} + + ); + } +} diff --git a/assets/lib/elements/FormTextInput.tsx b/assets/lib/elements/FormTextInput.tsx new file mode 100644 index 0000000..95f7717 --- /dev/null +++ b/assets/lib/elements/FormTextInput.tsx @@ -0,0 +1,52 @@ +import { ChangeEventHandler, Component } from "react"; +import React = require("react"); + +interface IProps { + inputId: string; + label: string; + disabled?: boolean; + placeholder: string; + onChange: ChangeEventHandler; + value: string; +} +interface IState {} + +export default class FormTextInput extends Component { + constructor(props) { + super(props); + + this.renderLabel = this.renderLabel.bind(this); + this.renderInput = this.renderInput.bind(this); + } + + renderLabel() { + return this.props.label ? ( + + ) : ( + "" + ); + } + + renderInput() { + return ( + + ); + } + + render() { + return ( + + {this.renderLabel()} + {this.renderInput()} + + ); + } +} diff --git a/assets/lib/elements/WarningBar.tsx b/assets/lib/elements/WarningBar.tsx new file mode 100644 index 0000000..d7032af --- /dev/null +++ b/assets/lib/elements/WarningBar.tsx @@ -0,0 +1,34 @@ +import { Component, MouseEventHandler } from "react"; +import React = require("react"); +import Button from "./Button"; + +interface IProps { + title: string; + innerText: string; + buttonText?: string; + buttonClick?: MouseEventHandler; +} +interface IState {} + +export default class WarningBar extends Component { + render() { + if (!this.props.title || !this.props.innerText) return null; + + return ( +
+

{this.props.title}

+

{this.props.innerText}

+ {this.props.hasOwnProperty("buttonText") && + this.props.hasOwnProperty("buttonClick") && ( +

+

+ ); + } +} diff --git a/assets/lib/forms/ControlForm.tsx b/assets/lib/forms/ControlForm.tsx new file mode 100644 index 0000000..34914d4 --- /dev/null +++ b/assets/lib/forms/ControlForm.tsx @@ -0,0 +1,126 @@ +import Button from "../elements/Button"; +import FormTextInput from "../elements/FormTextInput"; +import FormCancel from "../elements/FormCancel"; +import FormCheckbox from "../elements/FormCheckbox"; +import WarningBar from "../elements/WarningBar"; +import { hideModal } from "../components/Modal"; +import { Control as CustomizerControl, ControlType, DatabaseObject } from "../models/models"; +import { connect } from "react-redux"; +import React = require("react"); +import FormSelect from "../elements/FormSelect"; +import { + ControlTypeSelectOptions, + ControlTypesWithOptions, GetControlTypeById +} from "../models/selectOptions"; +import { ModalWrapper, ModalContent } from "../styled"; +import useControlForm from "../hooks/useControlForm"; + + + +interface IProps { + data: DatabaseObject; + control?: CustomizerControl; +} + +const ControlForm = (props: IProps) => { + const { + errorTitle, + errorMessage, + autoGenerateIdChange, + autoGenerateId, + controlDefault, + controlDefaultChange, + controlId, + controlIdChange, + controlType, + controlTypeChange, + controlLabel, + controlLabelChange, + controlChoices, + controlChoicesChange, + save, + } = useControlForm(props.data, props.control); + + const renderChoices = () => { + const selectedType = Number.parseInt(controlType.toString()); + const hasOptions = ControlTypesWithOptions.includes(selectedType); + + if (!hasOptions) { + return null; + } + + return ( + + ); + } + + return ( + + +

Create a New Customizer Control

+ + + + + + + + + {renderChoices()} + +
+