import axios from "axios"; import { useState } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Label } from "@/components/ui/label"; import { Maximize2, Minimize2, Trash2Icon } from "lucide-react"; import { ActionType } from "@prisma/client"; import { boolean, z } from "zod"; const actionTypes = [ { "name": "Overwrite local file content", "value": ActionType.WRITE_TO_FILE, "inputs": [ { "type": "text", "label": "File path", "key": "file_path", "placeholder": "Enter local file path, relative or full.", "required": true }, { "type": "text", "label": "File content", "key": "file_content", "placeholder": "Enter the text to write to the file.", "required": true } ] }, { "name": "Append to local file", "value": ActionType.APPEND_TO_FILE, "inputs": [ { "type": "text", "label": "File path", "key": "file_path", "placeholder": "Enter local file path, relative or full.", "required": true }, { "type": "text", "label": "File content", "key": "file_content", "placeholder": "Enter the text to append to the file.", "required": true } ] }, { "name": "Cause a transformation on OBS scene item", "value": ActionType.OBS_TRANSFORM, "inputs": [] }, { "name": "Play an audio file locally", "value": ActionType.AUDIO_FILE, "inputs": [ { "type": "text", "label": "File path", "key": "file_path", "placeholder": "Enter local file path, relative or full.", "required": true } ] }, { "name": "User gets a random TTS voice that is enabled", "value": ActionType.RANDOM_TTS_VOICE, "inputs": [] }, { "name": "User gets a specific TTS voice", "value": ActionType.SPECIFIC_TTS_VOICE, "inputs": [ { "type": "text", "label": "TTS Voice", "key": "tts_voice", "placeholder": "Name of an enabled TTS voice", "required": true } ] }, { "name": "Toggle OBS scene item visibility", "value": ActionType.TOGGLE_OBS_VISIBILITY, "inputs": [ { "type": "text", "label": "Scene Name", "key": "scene_name", "placeholder": "Name of the OBS scene", "required": true }, { "type": "text", "label": "Scene Item Name", "key": "scene_item_name", "placeholder": "Name of the OBS scene item / source", "required": true } ] }, { "name": "Set OBS scene item visibility", "value": ActionType.SPECIFIC_OBS_VISIBILITY, "inputs": [ { "type": "text", "label": "Scene Name", "key": "scene_name", "placeholder": "Name of the OBS scene", "required": true }, { "type": "text", "label": "Scene Item Name", "key": "scene_item_name", "placeholder": "Name of the OBS scene item / source", "required": true }, { "type": "text-values", "label": "Visible", "key": "obs_visible", "placeholder": "true for visible; false otherwise", "values": ["true", "false"], "required": true } ] }, { "name": "Set OBS scene item's index", "value": ActionType.SPECIFIC_OBS_INDEX, "inputs": [ { "type": "text", "label": "Scene Name", "key": "scene_name", "placeholder": "Name of the OBS scene", "required": true }, { "type": "text", "label": "Scene Item Name", "key": "scene_item_name", "placeholder": "Name of the OBS scene item / source", "required": true }, { "type": "number", "label": "Index", "key": "obs_index", "placeholder": "index, starting from 0.", "required": true } ] }, { "name": "Sleep - do nothing", "value": ActionType.SLEEP, "inputs": [ { "type": "number", "label": "Sleep", "key": "sleep", "placeholder": "Time in milliseconds to do nothing", "required": true } ] }, { "name": "Nightbot - Play", "value": ActionType.NIGHTBOT_PLAY, "inputs": [ { "type": "oauth.nightbot.play", "label": "nightbot.play", "key": "nightbot_play", "placeholder": "", "required": true } ] }, { "name": "Nightbot - Pause", "value": ActionType.NIGHTBOT_PAUSE, "inputs": [ { "type": "oauth.nightbot.pause", "label": "nightbot.pause", "key": "nightbot_pause", "placeholder": "", "required": true } ] }, { "name": "Nightbot - Skip", "value": ActionType.NIGHTBOT_SKIP, "inputs": [ { "type": "oauth.nightbot.skip", "label": "nightbot.skip", "key": "nightbot_skip", "placeholder": "", "required": true } ] }, { "name": "Nightbot - Clear Playlist", "value": ActionType.NIGHTBOT_CLEAR_PLAYLIST, "inputs": [ { "type": "oauth.nightbot.clear_playlist", "label": "nightbot.clear_playlist", "key": "nightbot_clear_playlist", "placeholder": "", "required": true } ] }, { "name": "Nightbot - Clear Queue", "value": ActionType.NIGHTBOT_CLEAR_QUEUE, "inputs": [ { "type": "oauth.nightbot.clear_queue", "label": "nightbot.clear_queue", "key": "nightbot_clear_queue", "placeholder": "", "required": true } ] }, ] const nameSchema = z.string({ required_error: "Name is required.", invalid_type_error: "Name must be a string" }).regex(/^[\w\-\s]{1,32}$/, "Name must contain only letters, numbers, spaces, dashes, and underscores.") interface RedeemableAction { name: string type: string | undefined data: { [key: string]: string } edit?: boolean showEdit?: boolean isNew: boolean obsTransformations: { label: string, placeholder: string, description: string }[] connections: { name: string, type: string }[] adder: (name: string, type: ActionType, data: { [key: string]: string }) => void remover: (action: { name: string, type: string, data: any }) => void } const RedemptionAction = ({ name, type, data, edit, showEdit = true, isNew = false, obsTransformations = [], connections = [], adder, remover }: RedeemableAction) => { const [open, setOpen] = useState<{ [key: string]: boolean }>({ 'actions': false, 'oauth': false, 'oauth.nightbot': false, 'oauth.twitch': false }) const [actionName, setActionName] = useState(name) const [actionType, setActionType] = useState<{ name: string, value: ActionType, inputs: any[] } | undefined>(actionTypes.find(a => a.value == type?.toUpperCase())) const [actionData, setActionData] = useState<{ [key: string]: string }>(data) const [isEditable, setIsEditable] = useState(edit) const [isMinimized, setIsMinimized] = useState(!isNew) const [oldData, setOldData] = useState<{ n: string, t: ActionType | undefined, d: { [k: string]: string } } | undefined>(undefined) const [error, setError] = useState(undefined) function Save(isNew: boolean) { setError(undefined) if (!actionName) { setError("Name is required.") return } const nameValidation = nameSchema.safeParse(actionName) if (!nameValidation.success) { setError(JSON.parse(nameValidation.error['message'])[0].message) return } if (!actionType) { setError("Action type is required.") return } if (!actionTypes.some(t => t.value == actionType.value)) { setError("Invalid action type given.") return } if (!actionData) { setError("Something went wrong with the data.") return } const inputs = actionTypes.find(a => a.value == actionType.value && a.name == actionType.name)!.inputs const required = inputs.filter(i => i.required) for (const input of required) { if (!(input.key in actionData)) { setError("The field '" + input.label + "' is required.") return } } let info: any = { name: actionName, type: actionType.value, } info.data = actionData if (isNew) { axios.post("/api/settings/redemptions/actions", info) .then(d => { adder(actionName, actionType.value, actionData) setActionName("") setActionType(undefined) setActionData({}) }) .catch(error => setError(error.response.data.message)) } else { axios.put("/api/settings/redemptions/actions", info) .then(d => { setIsEditable(false) }) .catch(error => setError(error.response.data.message)) } } function Cancel(data: { n: string, t: ActionType | undefined, d: { [k: string]: any } } | undefined) { setError(undefined) if (!data) return setActionName(data.n) setActionType(actionTypes.find(a => a.value == data.t)) setActionData(data.d) setIsEditable(false) setOldData(undefined) } function Delete() { axios.delete("/api/settings/redemptions/actions?action_name=" + actionName) .then(d => { remover(d.data) }) } return (
{isMinimized &&
|| !isMinimized &&
setActionName(e.target.value)} value={actionName} readOnly={!isNew} /> {!isEditable && || isEditable && setOpen({ ...open, 'actions': !open['actions'] })}> No action found. {actionTypes.map((action) => ( { setActionType(actionTypes.find(v => v.name.toLowerCase() == value.toLowerCase())) setOpen({ ...open, 'actions': false }) }}> {action.name} ))} }
{actionType &&
{actionType.inputs.map(i => { if (i.type == "text") { return
setActionData(d => { let abc = { ...actionData } abc[i.key] = e.target.value; return abc })} readOnly={!isEditable} />
} else if (i.type == "number") { return
setActionData(d => { let abc = { ...actionData } const v = parseInt(e.target.value) if (e.target.value.length == 0 || Number.isNaN(v)) { abc[i.key] = "0" } else if (!Number.isNaN(v) && Number.isSafeInteger(v)) { abc[i.key] = v.toString() } return abc })} readOnly={!isEditable} />
} else if (i.type == "text-values") { return
setActionData(d => { let abc = { ...actionData } abc[i.key] = i.values.map((v: string) => v.startsWith(e.target.value)).some((v: boolean) => v) ? e.target.value : abc[i.key] return abc })} readOnly={!isEditable} />
} else { return
{ const temp = { ...open }; temp[i.type] = !temp[i.type]; setOpen(temp) }}> No connection found. {connections.filter(c => !i.type.includes('.') || c.type == i.type.split('.')[1]) .map((connection) => ( { const connection = connections.find(v => v.name.toLowerCase() == value.toLowerCase()) if (!!connection) { setActionData({ 'oauth_name': connection.name, 'oauth_type': connection.type }) } else setActionData({}) const temp = { ...open } temp[i.type] = false setOpen(temp) }}> {connection.name} ))}
} })}
} {actionType && actionType.value == ActionType.OBS_TRANSFORM &&
{obsTransformations.map(t =>
{ let c = { ...actionData } c[t.label] = e.target.value setActionData(c) }} readOnly={!isEditable} />
)}
}
{error &&
{error}
}
{isEditable && } {isEditable && !isNew && } {showEdit && !isEditable && } {!isEditable && } {!isNew && }
}
); } export default RedemptionAction;