Skip to content

Commit ae8e430

Browse files
gettinToastygettinToasty
andauthored
Port Goal Widgets to React (#5761)
* Add GenericGoal.tsx * Add widget config * Fix window rendering * Fix FontFamily input * Remove old files * Fix compile * Restore base-goal.ts for StreamBoss * Add translations * Fix tests * Fix resubs rendering * Fix goal change settings tests * Fix dropdown labels * Fix tests again * Comment out color input changes * Add proper assertions * Fix pass condition * Name form --------- Co-authored-by: gettinToasty <sbeyer@logitech.com>
1 parent 86dc5ae commit ae8e430

33 files changed

Lines changed: 567 additions & 746 deletions

app/app-services.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,23 +114,14 @@ export { SseService } from 'services/server-sent-events';
114114

115115
// WIDGETS
116116
export { WidgetSource, WidgetsService } from './services/widgets';
117-
export { BitGoalService } from 'services/widgets/settings/bit-goal';
118-
export { DonationGoalService } from 'services/widgets/settings/donation-goal';
119-
export { FollowerGoalService } from 'services/widgets/settings/follower-goal';
120-
export { StarsGoalService } from 'services/widgets/settings/stars-goal';
121-
export { SupporterGoalService } from 'services/widgets/settings/supporter-goal';
122-
export { SubscriberGoalService } from 'services/widgets/settings/subscriber-goal';
123-
export { CharityGoalService } from 'services/widgets/settings/charity-goal';
124117
export { StreamBossService } from 'services/widgets/settings/stream-boss';
125118
export { CreditsService } from 'services/widgets/settings/credits';
126119
export { TipJarService } from 'services/widgets/settings/tip-jar';
127-
export { SubGoalService } from 'services/widgets/settings/sub-goal';
128120
export { MediaShareService } from 'services/widgets/settings/media-share';
129121
export { AlertBoxService } from 'services/widgets/settings/alert-box';
130122
export { SpinWheelService } from 'services/widgets/settings/spin-wheel';
131123
export { PollService } from 'services/widgets/settings/poll';
132124
export { ChatHighlightService } from 'services/widgets/settings/chat-highlight';
133-
export { SuperchatGoalService } from 'services/widgets/settings/superchat-goal';
134125

135126
import { AppService } from './services/app';
136127
import { WindowsService } from './services/windows';

app/components-react/shared/inputs/FormFactory.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@ function FormInput(p: {
101101

102102
if (!type) return <></>;
103103

104-
// TODO: index
105-
// @ts-ignore
106-
const Input = componentTable[type];
104+
const Input = componentTable[type as TInputType];
105+
if (!Input) throw new Error(`No component found for Input Type ${type}`);
106+
107107
let handleChange = p.onChange(p.id);
108108
if (type === 'checkboxGroup') handleChange = p.onChange;
109109
if (p.metadata.onChange) handleChange = p.metadata.onChange;
@@ -114,6 +114,7 @@ function FormInput(p: {
114114
{...p.metadata}
115115
name={p.id}
116116
value={p.values[p.id]}
117+
//@ts-ignore
117118
values={type === 'checkboxGroup' && p.values}
118119
onChange={handleChange}
119120
/>

app/components-react/shared/inputs/TextInput.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const ANT_INPUT_FEATURES = [
1212
'autoFocus',
1313
'prefix',
1414
'size',
15+
'placeholder',
1516
] as const;
1617

1718
export type TTextInputProps = TSlobsInputProps<

app/components-react/shared/inputs/metadata.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Rule } from 'antd/lib/form';
22
import { TInputValue } from './FormFactory';
33
import { IListOption } from './ListInput';
4+
import { TInputType } from './inputs';
45

56
/**
67
* Metadata generator for inputs
@@ -71,6 +72,8 @@ interface IBaseMetadata {
7172
interface ITextMetadata extends IBaseMetadata {
7273
value?: string;
7374
isPassword?: boolean;
75+
placeholder?: string;
76+
max?: number;
7477
}
7578

7679
export interface ICheckboxGroupMetadata extends IBaseMetadata {
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import React, { useState } from 'react';
2+
import { Button, Menu } from 'antd';
3+
import { $t } from 'services/i18n';
4+
import { IWidgetCommonState, useWidget, WidgetModule, WidgetParams } from './common/useWidget';
5+
import { WidgetLayout } from './common/WidgetLayout';
6+
import FormFactory, { TInputValue } from 'components-react/shared/inputs/FormFactory';
7+
import Form from 'components-react/shared/inputs/Form';
8+
import { metadata } from '../shared/inputs/metadata';
9+
import { WidgetType } from 'services/widgets';
10+
import { authorizedHeaders, jfetch } from 'util/requests';
11+
import { Services } from 'components-react/service-provider';
12+
13+
interface IGoalState extends IWidgetCommonState {
14+
data: {
15+
goal: {
16+
title: string;
17+
goal_amount: number;
18+
current_amount: number;
19+
to_go: string;
20+
} | null;
21+
settings: {
22+
background_color: string;
23+
bar_color: string;
24+
bar_bg_color: string;
25+
text_color: string;
26+
bar_text_color: string;
27+
font: string;
28+
bar_thickness: string;
29+
layout: string;
30+
custom_enabled: boolean;
31+
custom_html: string;
32+
custom_js: string;
33+
custom_css: string;
34+
};
35+
custom_defaults: {
36+
html: string;
37+
js: string;
38+
css: string;
39+
};
40+
has_goal: boolean;
41+
show_bar: string;
42+
};
43+
}
44+
45+
export function GenericGoal() {
46+
const {
47+
isLoading,
48+
settings,
49+
createGoalMeta,
50+
goalSettings,
51+
visualMeta,
52+
updateSetting,
53+
setSelectedTab,
54+
selectedTab,
55+
saveGoal,
56+
type,
57+
} = useGenericGoal();
58+
59+
const isCharity = type === WidgetType.CharityGoal;
60+
61+
const hasGoal = !!goalSettings;
62+
63+
const [goalCreateValues, setGoalCreateValues] = useState<Dictionary<TInputValue>>({
64+
title: '',
65+
goal_amount: 100,
66+
manual_goal_amount: 0,
67+
ends_at: '',
68+
});
69+
70+
function updateGoalCreate(key: string) {
71+
return (val: TInputValue) => {
72+
setGoalCreateValues({ ...goalCreateValues, [key]: val });
73+
};
74+
}
75+
76+
return (
77+
<WidgetLayout>
78+
<Menu onClick={e => setSelectedTab(e.key)} selectedKeys={[selectedTab]}>
79+
<Menu.Item key="general">{$t('Visual Settings')}</Menu.Item>
80+
{!isCharity && <Menu.Item key="goal">{$t('Goal Settings')}</Menu.Item>}
81+
</Menu>
82+
<Form>
83+
{!isLoading && selectedTab === 'goal' && !hasGoal && (
84+
<>
85+
<FormFactory
86+
metadata={createGoalMeta}
87+
values={goalCreateValues}
88+
onChange={updateGoalCreate}
89+
/>
90+
<Button className="button button--action" onClick={() => saveGoal(goalCreateValues)}>
91+
{$t('Save Goal')}
92+
</Button>
93+
</>
94+
)}
95+
{!isLoading && selectedTab === 'goal' && hasGoal && <DisplayGoal goal={goalSettings} />}
96+
{!isLoading && selectedTab === 'general' && (
97+
<FormFactory
98+
metadata={visualMeta}
99+
values={settings}
100+
onChange={updateSetting}
101+
name="visualSettingsForm"
102+
/>
103+
)}
104+
</Form>
105+
</WidgetLayout>
106+
);
107+
}
108+
109+
function DisplayGoal(p: { goal: IGoalState['data']['goal'] }) {
110+
const { resetGoal } = useGenericGoal();
111+
112+
if (!p.goal) return <></>;
113+
return (
114+
<div className="section__body">
115+
<div className="goal-row">
116+
<span>{$t('Title')}</span>
117+
<span>{p.goal.title}</span>
118+
</div>
119+
<div className="goal-row">
120+
<span>{$t('Goal Amount')}</span>
121+
<span>{p.goal.goal_amount}</span>
122+
</div>
123+
<div className="goal-row">
124+
<span>{$t('Current Amount')}</span>
125+
<span>{p.goal.current_amount}</span>
126+
</div>
127+
<div className="goal-row">
128+
<span>{$t('Days Remaining')}</span>
129+
<span>{p.goal.to_go}</span>
130+
</div>
131+
<Button className="button button--soft-warning" onClick={resetGoal}>
132+
{$t('End Goal')}
133+
</Button>
134+
</div>
135+
);
136+
}
137+
138+
export class GenericGoalModule extends WidgetModule<IGoalState> {
139+
get dateValidator() {
140+
return {
141+
// regex from https://stackoverflow.com/questions/2520633/what-is-the-mm-dd-yyyy-regular-expression-and-how-do-i-use-it-in-php
142+
pattern: /^(0[1-9]|1[012])[/](0[1-9]|[12][0-9]|3[01])[/](19|20)\d\d$/,
143+
message: $t('Must be in MM/DD/YYYY format.'),
144+
};
145+
}
146+
147+
get createGoalMeta() {
148+
return {
149+
title: metadata.text({
150+
label: $t('Title'),
151+
required: true,
152+
max: 60,
153+
}),
154+
goal_amount: metadata.number({
155+
label: $t('Goal Amount'),
156+
required: true,
157+
min: 1,
158+
}),
159+
manual_goal_amount: metadata.number({
160+
label: $t('Starting Amount'),
161+
min: 0,
162+
}),
163+
ends_at: metadata.text({
164+
label: $t('End After'),
165+
required: true,
166+
placeholder: 'MM/DD/YYYY',
167+
rules: [this.dateValidator],
168+
}),
169+
};
170+
}
171+
172+
get goalSettings() {
173+
return this.widgetData.goal;
174+
}
175+
176+
get visualMeta() {
177+
const meta = {
178+
layout: metadata.list({
179+
label: $t('Layout'),
180+
options: [
181+
{ label: $t('Standard'), value: 'standard' },
182+
{ label: $t('Condensed'), value: 'condensed' },
183+
],
184+
}),
185+
background_color: metadata.color({ label: $t('Background Color') }),
186+
bar_color: metadata.color({ label: $t('Bar Color') }),
187+
bar_bg_color: metadata.color({ label: $t('Bar Background Color') }),
188+
text_color: metadata.color({
189+
label: $t('Text Color'),
190+
tooltip: $t('A hex code for the base text color.'),
191+
}),
192+
bar_text_color: metadata.color({ label: $t('Bar Text Color') }),
193+
bar_thickness: metadata.slider({ label: $t('Bar Thickness'), min: 32, max: 128, step: 4 }),
194+
font: { type: 'fontFamily', label: $t('Font Family') },
195+
};
196+
197+
if (this.state.type === WidgetType.SubGoal) {
198+
return { include_resubs: metadata.switch({ label: $t('Include Resubs') }), ...meta };
199+
}
200+
return meta;
201+
}
202+
203+
get headers() {
204+
return authorizedHeaders(
205+
Services.UserService.apiToken,
206+
new Headers({ 'Content-Type': 'application/json' }),
207+
);
208+
}
209+
210+
resetGoal() {
211+
const url = this.config.goalUrl;
212+
if (!url) return;
213+
jfetch(new Request(url, { method: 'DELETE', headers: this.headers }));
214+
}
215+
216+
saveGoal(options: Dictionary<TInputValue>) {
217+
const url = this.config.goalUrl;
218+
if (!url) return;
219+
jfetch(
220+
new Request(url, {
221+
method: 'POST',
222+
headers: this.headers,
223+
body: JSON.stringify(options),
224+
}),
225+
);
226+
}
227+
228+
patchAfterFetch(data: IGoalState['data']): IGoalState['data'] {
229+
// fix a bug when API returning an empty array instead of null
230+
if (Array.isArray(data.goal)) data.goal = null;
231+
return data;
232+
}
233+
}
234+
235+
function useGenericGoal() {
236+
return useWidget<GenericGoalModule>();
237+
}

app/components-react/widgets/common/WidgetWindow.tsx

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,7 @@ import { AlertBox } from '../AlertBox';
66
import { AlertBoxModule } from '../useAlertBox';
77
import { useWidgetRoot, WidgetModule } from './useWidget';
88
// TODO: import other widgets here to avoid merge conflicts
9-
// BitGoal
10-
// DonationGoal
11-
// CharityGoal
12-
// FollowerGoal
13-
// StarsGoal
14-
// SubGoal
15-
// SubscriberGoal
9+
import { GenericGoal, GenericGoalModule } from '../GenericGoal';
1610
import { ChatBox, ChatBoxModule } from '../ChatBox';
1711
// ChatHighlight
1812
// Credits
@@ -36,13 +30,13 @@ import { useChildWindowParams } from 'components-react/hooks';
3630
// define list of Widget components and modules
3731
export const components = {
3832
AlertBox: [AlertBox, AlertBoxModule],
39-
// BitGoal
40-
// DonationGoal
41-
// CharityGoal
42-
// FollowerGoal
43-
// StarsGoal
44-
// SubGoal
45-
// SubscriberGoal
33+
BitGoal: [GenericGoal, GenericGoalModule],
34+
DonationGoal: [GenericGoal, GenericGoalModule],
35+
CharityGoal: [GenericGoal, GenericGoalModule],
36+
FollowerGoal: [GenericGoal, GenericGoalModule],
37+
StarsGoal: [GenericGoal, GenericGoalModule],
38+
SubGoal: [GenericGoal, GenericGoalModule],
39+
SubscriberGoal: [GenericGoal, GenericGoalModule],
4640
ChatBox: [ChatBox, ChatBoxModule],
4741
// ChatHighlight
4842
// Credits

app/components-react/widgets/common/useWidget.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -497,8 +497,8 @@ export function useWidgetRoot<T extends typeof WidgetModule>(Module: T, params:
497497
/**
498498
* Returns the widget's module from the existing context and selects requested fields
499499
*/
500-
export function useWidget<TModule extends WidgetModule>() {
501-
return useModule('WidgetModule') as GetUseModuleResult<TModule>;
500+
export function useWidget<TModule extends WidgetModule>(params?: WidgetParams) {
501+
return useModule('WidgetModule', params ? [params] : undefined) as GetUseModuleResult<TModule>;
502502
}
503503

504504
/**

app/components/widgets/goal/BitGoal.tsx

Lines changed: 0 additions & 10 deletions
This file was deleted.

app/components/widgets/goal/CharityGoal.tsx

Lines changed: 0 additions & 10 deletions
This file was deleted.

app/components/widgets/goal/DonationGoal.tsx

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)