Added redemptions & redeemable actions. Fixed a few bugs.

This commit is contained in:
Tom
2024-06-24 22:16:55 +00:00
parent 68df045c54
commit 6548ce33e0
35 changed files with 1787 additions and 471 deletions

View File

@@ -3,63 +3,46 @@
import axios from "axios";
import { Button } from "@/components/ui/button";
import * as React from 'react';
import { ApiKey, User } from "@prisma/client";
import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
const SettingsPage = () => {
const { data: session, status } = useSession();
const [apiKeyViewable, setApiKeyViewable] = useState(0)
const [apiKeyChanges, setApiKeyChanges] = useState(0)
const [apiKeys, setApiKeys] = useState<ApiKey[]>([])
const ApiKeyPage = () => {
const [apiKeyViewable, setApiKeyViewable] = useState<number>(-1)
const [apiKeys, setApiKeys] = useState<{ id: string, label: string, userId: string }[]>([])
useEffect(() => {
const fetchData = async () => {
try {
const keys = (await axios.get("/api/tokens")).data ?? {};
setApiKeys(keys)
} catch (error) {
console.log("ERROR", error)
}
await axios.get("/api/tokens")
.then(d => setApiKeys(d.data ?? []))
.catch(console.error)
};
fetchData().catch(console.error);
}, [apiKeyChanges]);
fetchData();
}, []);
const onApiKeyAdd = async () => {
try {
await axios.post("/api/token", {
label: "Key label"
});
setApiKeyChanges(apiKeyChanges + 1)
} catch (error) {
console.log("ERROR", error)
}
const onApiKeyAdd = async (label: string) => {
await axios.post("/api/token", { label })
.then(d => setApiKeys(apiKeys.concat([d.data])))
.catch(console.error)
}
const onApiKeyDelete = async (id: string) => {
try {
await axios.delete("/api/token/" + id);
setApiKeyChanges(apiKeyChanges - 1)
} catch (error) {
console.log("ERROR", error)
}
await axios.delete("/api/token/" + id)
.then((d) => setApiKeys(apiKeys.filter(k => k.id != d.data.id)))
.catch(console.error)
}
return (
<div>
<div className="px-10 py-5 mx-5 my-10">
<div>
<div className="text-xl justify-left mt-10">API Keys</div>
<Table className="max-w-2xl">
<div className="text-xl justify-left mt-10 text-center">API Keys</div>
<Table>
<TableCaption>A list of your secret API keys.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Label</TableHead>
<TableHead>Token</TableHead>
<TableHead>View</TableHead>
<TableHead>Action</TableHead>
</TableRow>
</TableHeader>
@@ -67,20 +50,20 @@ const SettingsPage = () => {
{apiKeys.map((key, index) => (
<TableRow key={key.id}>
<TableCell className="font-medium">{key.label}</TableCell>
<TableCell>{(apiKeyViewable & (1 << index)) > 0 ? key.id : "*".repeat(key.id.length)}</TableCell>
<TableCell>{apiKeyViewable == index ? key.id : "*".repeat(key.id.length)}</TableCell>
<TableCell>
<Button onClick={() => setApiKeyViewable((v) => v ^ (1 << index))}>
{(apiKeyViewable & (1 << index)) > 0 ? "HIDE" : "VIEW"}
<Button onClick={() => setApiKeyViewable((v) => v != index ? index : -1)}>
{apiKeyViewable == index ? "HIDE" : "VIEW"}
</Button>
<Button onClick={() => onApiKeyDelete(key.id)} className="ml-[10px] bg-red-500 hover:bg-red-700">DELETE</Button>
</TableCell>
<TableCell><Button onClick={() => onApiKeyDelete(key.id)}>DEL</Button></TableCell>
<TableCell></TableCell>
</TableRow>
))}
<TableRow key="ADD">
<TableCell className="font-medium"></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell><Button onClick={onApiKeyAdd}>ADD</Button></TableCell>
<TableCell><Button onClick={() => onApiKeyAdd("Key label")}>ADD</Button></TableCell>
</TableRow>
</TableBody>
</Table>
@@ -90,4 +73,4 @@ const SettingsPage = () => {
);
}
export default SettingsPage;
export default ApiKeyPage;

View File

@@ -10,7 +10,7 @@ import Link from "next/link";
import { cn } from "@/lib/utils";
import { Skeleton } from "@/components/ui/skeleton";
const SettingsPage = () => {
const ConnectionsPage = () => {
const { data: session, status } = useSession();
const [previousUsername, setPreviousUsername] = useState<string>()
const [userId, setUserId] = useState<string>()
@@ -24,7 +24,7 @@ const SettingsPage = () => {
setPreviousUsername(session.user?.name as string)
if (session.user?.name) {
const fetchData = async () => {
var connection: User = (await axios.get("/api/account")).data
let connection: User = (await axios.get("/api/account")).data
setUserId(connection.id)
setLoading(false)
}
@@ -36,7 +36,7 @@ const SettingsPage = () => {
const [twitchUser, setTwitchUser] = useState<TwitchConnection | null>(null)
useEffect(() => {
const fetchData = async () => {
var connection: TwitchConnection = (await axios.get("/api/settings/connections/twitch")).data
let connection: TwitchConnection = (await axios.get("/api/settings/connections/twitch")).data
setTwitchUser(connection)
}
@@ -97,4 +97,4 @@ const SettingsPage = () => {
);
}
export default SettingsPage;
export default ConnectionsPage;

View File

@@ -3,8 +3,11 @@ import { cn } from "@/lib/utils";
import { headers } from 'next/headers';
import React from "react";
const SettingsLayout = async (
{ children } : { children:React.ReactNode } ) => {
const SettingsLayout = async ({
children
} : {
children:React.ReactNode
} ) => {
const headersList = headers();
const header_url = headersList.get('x-url') || "";
@@ -14,7 +17,7 @@ const SettingsLayout = async (
header_url.endsWith("/settings") && "flex h-full w-full md:w-[250px] z-30 flex-col fixed inset-y-0")}>
<SettingsNavigation />
</div>
<main className={cn("md:pl-[250px] h-full", header_url.endsWith("/settings") && "hidden")}>
<main className={"md:pl-[250px] h-full"}>
{children}
</main>
</div>

View File

@@ -0,0 +1,143 @@
"use client";
import axios from "axios";
import * as React from 'react';
import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import RedeemptionAction from "@/components/elements/redeemable-action";
import OBSRedemption from "@/components/elements/redemption";
import { ActionType } from "@prisma/client";
import InfoNotice from "@/components/elements/info-notice";
const obsTransformations = [
{ label: "scene_name", description: "", placeholder: "Name of the OBS scene" },
{ label: "scene_item_name", description: "", placeholder: "Name of the OBS scene item / source" },
{ label: "rotation", description: "", placeholder: "An expression using x as the previous value" },
{ label: "position_x", description: "", placeholder: "An expression using x as the previous value" },
{ label: "position_y", description: "", placeholder: "An expression using x as the previous value" }
]
const RedemptionsPage = () => {
const { data: session, status } = useSession();
const [previousUsername, setPreviousUsername] = useState<string | null>()
const [loading, setLoading] = useState<boolean>(true)
const [open, setOpen] = useState(false)
const [actions, setActions] = useState<{ name: string, type: string, data: any }[]>([])
const [twitchRedemptions, setTwitchRedemptions] = useState<{ id: string, title: string }[]>([])
const [redemptions, setRedemptions] = useState<{ id: string, redemptionId: string, actionName: string, order: number }[]>([])
function addAction(name: string, type: ActionType, data: { [key: string]: string }) {
setActions([...actions, { name, type, data }])
}
function removeAction(action: { name: string, type: string, data: any }) {
setActions(actions.filter(a => a.name != action.name))
}
function addRedemption(id: string, actionName: string, redemptionId: string, order: number) {
setRedemptions([...redemptions, { id, redemptionId, actionName, order }])
}
function removeRedemption(redemption: { id: string, redemptionId: string, actionName: string, order: number }) {
setRedemptions(redemptions.filter(r => r.id != redemption.id))
}
useEffect(() => {
if (status !== "authenticated" || previousUsername == session.user?.name) {
return
}
setPreviousUsername(session.user?.name)
axios.get("/api/settings/redemptions/actions")
.then(d => {
setActions(d.data)
})
axios.get("/api/account/redemptions")
.then(d => {
const rs = d.data.data?.map(r => ({ id: r.id, title: r.title })) ?? []
setTwitchRedemptions(rs)
axios.get("/api/settings/redemptions")
.then(d => {
setRedemptions(d.data)
})
})
}, [session])
return (
<div>
<div className="text-2xl text-center pt-[50px]">Redemption Actions</div>
<InfoNotice
message="Redemption actions are activated when specific Twitch channel point redeems have been activated. Aforementioned redeem need to be linked in the redemption part, together with the action, for the action to activate."
hidden={false} />
{actions.map(action =>
<div
className="px-10 py-3 w-full h-full flex-grow inset-y-1/2"
key={action.name}>
<RedeemptionAction
name={action.name}
type={action.type}
data={action.data}
edit={false}
showEdit={true}
isNew={false}
obsTransformations={obsTransformations}
adder={addAction}
remover={removeAction} />
</div>
)}
<div
className="px-10 py-3 w-full h-full flex-grow inset-y-1/2">
<RedeemptionAction
name=""
type={undefined}
data={{}}
edit={true}
showEdit={false}
isNew={true}
obsTransformations={obsTransformations}
adder={addAction}
remover={removeAction} />
</div>
<div className="text-2xl text-center pt-[50px]">Redemptions</div>
<InfoNotice
message="Redemptions are just a way to link specific actions to actual Twitch channel point redeems."
hidden={false} />
{redemptions.map(redemption =>
<div
className="px-10 py-3 w-full h-full flex-grow inset-y-1/2"
key={redemption.id}>
<OBSRedemption
id={redemption.id}
redemptionId={redemption.redemptionId}
actionName={redemption.actionName}
edit={false}
showEdit={true}
isNew={false}
actions={actions.map(a => a.name)}
twitchRedemptions={twitchRedemptions}
adder={addRedemption}
remover={removeRedemption} />
</div>
)}
<div
className="px-10 py-3 w-full h-full flex-grow inset-y-1/2">
<OBSRedemption
id={undefined}
redemptionId={undefined}
actionName=""
edit={true}
showEdit={false}
isNew={true}
actions={actions.map(a => a.name)}
twitchRedemptions={twitchRedemptions}
adder={addRedemption}
remover={removeRedemption} />
</div>
</div>
);
}
export default RedemptionsPage;

View File

@@ -230,7 +230,7 @@ const TTSFiltersPage = () => {
<Form {...usernameFilteredForm}>
<form onSubmit={usernameFilteredForm.handleSubmit(onAdd)}>
<div className="flex w-full items-center justify-between rounded-md border px-4 py-2 gap-3 mt-2">
<Label className="rounded-lg bg-primary px-2 py-1 text-xs text-primary-foreground ">
<Label className="rounded-lg bg-primary px-2 py-1 text-xs text-primary-foreground">
{tag}
</Label>
<FormField

View File

@@ -3,10 +3,8 @@
import axios from "axios";
import * as React from 'react';
import { Check, ChevronsUpDown } from "lucide-react"
import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { useEffect, useReducer, useState } from "react";
import { cn } from "@/lib/utils";
import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button"
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
@@ -16,49 +14,50 @@ import voices from "@/data/tts";
import InfoNotice from "@/components/elements/info-notice";
const TTSVoiceFiltersPage = () => {
const { data: session, status } = useSession();
const [loading, setLoading] = useState<boolean>(true)
const [open, setOpen] = useState(false)
const [value, setValue] = useState(0)
const [enabled, setEnabled] = useState(0)
const [defaultVoice, setDefaultVoice] = useState("")
const [loading, setLoading] = useState(true)
function enabledVoicesReducer(enabledVoices: { [voice: string]: boolean }, action: { type: string, value: string }) {
if (action.type == "enable") {
return { ...enabledVoices, [action.value]: true }
} else if (action.type == "disable") {
return { ...enabledVoices, [action.value]: false }
}
return enabledVoices
}
const [enabledVoices, dispatchEnabledVoices] = useReducer(enabledVoicesReducer, Object.assign({}, ...voices.map(v => ({[v]: false}) )))
useEffect(() => {
axios.get("/api/settings/tts/default")
.then((voice) => {
setValue(Number.parseInt(voice.data.value))
setDefaultVoice(voice.data)
})
axios.get("/api/settings/tts")
.then((d) => {
const total = d.data.reduce((acc: number, item: {value: number, label: string, gender: string, language: string}) => acc |= 1 << (item.value - 1), 0)
setEnabled(total)
const data: string[] = d.data;
data.forEach(d => dispatchEnabledVoices({ type: "enable", value: d }))
setLoading(false)
})
}, [])
const onDefaultChange = (voice: string) => {
try {
axios.post("/api/settings/tts/default", { voice })
.then(d => {
console.log(d)
})
.catch(e => console.error(e))
} catch (error) {
console.log("[TTS/DEFAULT]", error);
return;
}
}
const onEnabledChanged = (val: number) => {
const onEnabledChanged = (voice: string, state: boolean) => {
try {
axios.post("/api/settings/tts", { voice: val })
.then(d => {
console.log(d)
})
axios.post("/api/settings/tts", { voice: voice, state: state })
.catch(e => console.error(e))
} catch (error) {
console.log("[TTS]", error);
return;
console.log("[TTS/ENABLED]", error);
}
}
@@ -78,7 +77,7 @@ const TTSVoiceFiltersPage = () => {
role="combobox"
aria-expanded={open}
className="w-[200px] justify-between">
{value ? voices.find(v => Number.parseInt(v.value) == value)?.label : "Select voice..."}
{defaultVoice ? voices.find(v => v == defaultVoice) : "Select voice..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
@@ -89,20 +88,20 @@ const TTSVoiceFiltersPage = () => {
<CommandGroup>
{voices.map((voice) => (
<CommandItem
key={voice.value + "-" + voice.label}
value={voice.value}
onSelect={(currentValue) => {
setValue(Number.parseInt(currentValue))
onDefaultChange(voice.label)
key={voice}
value={voice}
onSelect={(currentVoice) => {
setDefaultVoice(voice)
onDefaultChange(voice)
setOpen(false)
}}>
<Check
className={cn(
"mr-2 h-4 w-4",
value === Number.parseInt(voice.value) ? "opacity-100" : "opacity-0"
defaultVoice === voice ? "opacity-100" : "opacity-0"
)}
/>
{voice.label}
{voice}
</CommandItem>
))}
</CommandGroup>
@@ -116,14 +115,14 @@ const TTSVoiceFiltersPage = () => {
<InfoNotice message="Voices can be disabled from being used. Default voice will always work." hidden={false} />
<div className="grid grid-cols-4 grid-flow-row gap-4 pt-[20px]">
{voices.map((v, i) => (
<div key={v.label + "-enabled"} className="h-[30px] row-span-1 col-span-1 align-middle flex items-center justify-center">
<div key={v + "-enabled"} className="h-[30px] row-span-1 col-span-1 align-middle flex items-center justify-center">
<Checkbox onClick={() => {
const newVal = enabled ^ (1 << (Number.parseInt(v.value) - 1))
setEnabled(newVal)
onEnabledChanged(newVal)
dispatchEnabledVoices({ type: enabledVoices[v] ? "disable" : "enable", value: v })
onEnabledChanged(v, !enabledVoices[v])
}}
checked={(enabled & (1 << (Number.parseInt(v.value) - 1))) > 0} />
<div className="pl-[5px]">{v.label}</div>
disabled={loading}
checked={enabledVoices[v]} />
<div className="pl-[5px]">{v}</div>
</div>
))}
</div>