Skip to content

Commit 89e98f9

Browse files
johnebgoodclaude
andcommitted
Add cloud settings save/load via DynamoDB
- New GET/PUT /settings/{channel} routes on api.tts.bot - tts-bot Lambda handles new routes, storing settings in tts.bot table under login="streamer_settings" keyed by channel - PUT validates Twitch access token matches the channel - settingsApiService.ts for frontend API calls - SettingsPanel gets ↑ Save and ↓ Load buttons with status feedback Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 128b135 commit 89e98f9

10 files changed

Lines changed: 100 additions & 3 deletions

react-app/src/components/settings/SettingsPanel.tsx

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState } from 'react';
2-
import { useSettingsStore, useVoiceStore, useAppearanceStore } from '../../store';
2+
import { useSettingsStore, useVoiceStore, useAppearanceStore, useAuthStore } from '../../store';
33
import { VoiceSelect } from '../shared/VoiceSelect';
4+
import { loadSettingsFromApi, saveSettingsToApi } from '../../services/settingsApiService';
45

56
interface SettingsPanelProps {
67
channel: string;
@@ -11,9 +12,34 @@ type Tab = 'general' | 'voices' | 'translation' | 'filters' | 'appearance' | 'we
1112

1213
export function SettingsPanel({ channel, onClose }: SettingsPanelProps) {
1314
const [tab, setTab] = useState<Tab>('general');
15+
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'loading' | 'ok' | 'err'>('idle');
1416
const settings = useSettingsStore();
1517
const voice = useVoiceStore();
1618
const appearance = useAppearanceStore();
19+
const { accessToken, twitchUsername } = useAuthStore();
20+
21+
const handleSave = async () => {
22+
setSaveStatus('saving');
23+
const payload = { ...settings, ...voice };
24+
const ok = await saveSettingsToApi(twitchUsername || channel, payload as Record<string, unknown>, accessToken);
25+
setSaveStatus(ok ? 'ok' : 'err');
26+
setTimeout(() => setSaveStatus('idle'), 2000);
27+
};
28+
29+
const handleLoad = async () => {
30+
setSaveStatus('loading');
31+
const data = await loadSettingsFromApi(twitchUsername || channel);
32+
if (data) {
33+
Object.entries(data).forEach(([k, v]) => {
34+
if (k in settings) settings.setSetting(k as any, v as any);
35+
else if (k in voice) voice.setVoice(k as any, v as any);
36+
});
37+
setSaveStatus('ok');
38+
} else {
39+
setSaveStatus('err');
40+
}
41+
setTimeout(() => setSaveStatus('idle'), 2000);
42+
};
1743

1844
const cb = (key: keyof typeof settings) => (
1945
<input
@@ -42,7 +68,27 @@ export function SettingsPanel({ channel, onClose }: SettingsPanelProps) {
4268
<div className="settings-panel bg-dark text-white p-3 overflow-y-auto h-100">
4369
<div className="d-flex justify-content-between align-items-center mb-3">
4470
<h5 className="mb-0">Settings</h5>
45-
<button className="btn btn-sm btn-outline-secondary" onClick={onClose}></button>
71+
<div className="d-flex gap-1 align-items-center">
72+
{saveStatus === 'ok' && <span className="text-success small"></span>}
73+
{saveStatus === 'err' && <span className="text-danger small"></span>}
74+
<button
75+
className="btn btn-sm btn-outline-info"
76+
onClick={handleLoad}
77+
disabled={saveStatus === 'saving' || saveStatus === 'loading'}
78+
title="Load settings from cloud"
79+
>
80+
{saveStatus === 'loading' ? '…' : '↓ Load'}
81+
</button>
82+
<button
83+
className="btn btn-sm btn-outline-success"
84+
onClick={handleSave}
85+
disabled={saveStatus === 'saving' || saveStatus === 'loading'}
86+
title="Save settings to cloud"
87+
>
88+
{saveStatus === 'saving' ? '…' : '↑ Save'}
89+
</button>
90+
<button className="btn btn-sm btn-outline-secondary" onClick={onClose}></button>
91+
</div>
4692
</div>
4793

4894
<ul className="nav nav-pills nav-fill mb-3 flex-wrap gap-1">
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const API_BASE = 'https://api.tts.bot';
2+
3+
export async function loadSettingsFromApi(channel: string): Promise<Record<string, unknown> | null> {
4+
try {
5+
const res = await fetch(`${API_BASE}/settings/${channel}`);
6+
if (!res.ok) return null;
7+
const data = await res.json();
8+
return data?.settings ?? null;
9+
} catch {
10+
return null;
11+
}
12+
}
13+
14+
export async function saveSettingsToApi(
15+
channel: string,
16+
settings: Record<string, unknown>,
17+
accessToken: string
18+
): Promise<boolean> {
19+
try {
20+
const res = await fetch(`${API_BASE}/settings/${channel}`, {
21+
method: 'PUT',
22+
headers: { 'Content-Type': 'application/json' },
23+
body: JSON.stringify({ access_token: accessToken, settings }),
24+
});
25+
return res.ok;
26+
} catch {
27+
return false;
28+
}
29+
}

webfront/assets/CCTOverlay-BP4FVDpC.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webfront/assets/LoginPage-BxdDMbni.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webfront/assets/StreamerDashboard-BtTLoyvt.js

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webfront/assets/ViewerPage-BAAX-O_9.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webfront/assets/VoiceSelect-C3uyfMBU.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)