Added redemptions & redeemable actions. Fixed a few bugs.
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
143
app/settings/redemptions/page.tsx
Normal file
143
app/settings/redemptions/page.tsx
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user