feat: add settings and access dashboard slice
This commit is contained in:
1 parent
dcf2a1200c
commit
79de6aa2a1
11 files changed
+1066
No files matched your search
@@ -0,0 +1,209 @@
|
||||
import type { ConfigResponse } from '../../../../../packages/api-contracts/src/index.js';
|
||||
import type { SettingsDraft, SubredditDraft } from './settings-model.js';
|
||||
|
||||
interface SectionProps {
|
||||
draft: SettingsDraft;
|
||||
errors: Record<string, string>;
|
||||
onChange(next: SettingsDraft): void;
|
||||
}
|
||||
|
||||
interface NumberFieldProps {
|
||||
error?: string;
|
||||
label: string;
|
||||
name: string;
|
||||
min?: number;
|
||||
value: string;
|
||||
onChange(value: string): void;
|
||||
}
|
||||
|
||||
function NumberField({ error, label, name, min, value, onChange }: NumberFieldProps) {
|
||||
const errorId = `${name}-error`;
|
||||
return (
|
||||
<label className="settings-field">
|
||||
<span>{label}</span>
|
||||
<input
|
||||
aria-describedby={error ? errorId : undefined}
|
||||
aria-invalid={error ? 'true' : undefined}
|
||||
aria-label={label}
|
||||
min={min}
|
||||
name={name}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
step="1"
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
{error ? <small className="field-error" id={errorId}>{error}</small> : null}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function MonitoringSection({ draft, errors, onChange }: SectionProps) {
|
||||
return (
|
||||
<section aria-labelledby="monitoring-heading" className="panel settings-section">
|
||||
<div className="panel-heading"><div><span className="eyebrow">Discovery</span><h2 id="monitoring-heading">Monitoring</h2></div></div>
|
||||
<label className="toggle-field">
|
||||
<input
|
||||
aria-label="Prioritise recent posts"
|
||||
checked={draft.prioritizeRecentPosts}
|
||||
onChange={(event) => onChange({ ...draft, prioritizeRecentPosts: event.target.checked })}
|
||||
type="checkbox"
|
||||
/>
|
||||
<span><strong>Prioritise recent posts</strong><small>Prefer posts inside the configured recent window.</small></span>
|
||||
</label>
|
||||
<NumberField
|
||||
error={errors.recentWindowMinutes}
|
||||
label="Recent window in minutes"
|
||||
min={1}
|
||||
name="recent-window"
|
||||
onChange={(recentWindowMinutes) => onChange({ ...draft, recentWindowMinutes })}
|
||||
value={draft.recentWindowMinutes}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function RateLimitsSection({ draft, errors, onChange }: SectionProps) {
|
||||
return (
|
||||
<section aria-labelledby="rate-limits-heading" className="panel settings-section">
|
||||
<div className="panel-heading"><div><span className="eyebrow">Sending safeguards</span><h2 id="rate-limits-heading">Rate limits</h2></div></div>
|
||||
<div className="settings-grid">
|
||||
<NumberField
|
||||
error={errors.minDelayMinutes}
|
||||
label="Minimum delay in minutes"
|
||||
min={0}
|
||||
name="minimum-delay"
|
||||
onChange={(minDelayMinutes) => onChange({ ...draft, minDelayMinutes })}
|
||||
value={draft.minDelayMinutes}
|
||||
/>
|
||||
<NumberField
|
||||
error={errors.maxMessagesPerHour}
|
||||
label="Maximum messages per hour"
|
||||
min={1}
|
||||
name="hourly-limit"
|
||||
onChange={(maxMessagesPerHour) => onChange({ ...draft, maxMessagesPerHour })}
|
||||
value={draft.maxMessagesPerHour}
|
||||
/>
|
||||
<NumberField
|
||||
error={errors.jitterSeconds}
|
||||
label="Random jitter in seconds"
|
||||
min={0}
|
||||
name="jitter"
|
||||
onChange={(jitterSeconds) => onChange({ ...draft, jitterSeconds })}
|
||||
value={draft.jitterSeconds}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function updateSubreddit(draft: SettingsDraft, index: number, next: SubredditDraft): SettingsDraft {
|
||||
return { ...draft, subreddits: draft.subreddits.map((item, itemIndex) => itemIndex === index ? next : item) };
|
||||
}
|
||||
|
||||
export function BehaviourSection({ config, draft, errors, onChange }: SectionProps & { config: ConfigResponse }) {
|
||||
return (
|
||||
<section aria-labelledby="behaviour-heading" className="panel settings-section">
|
||||
<div className="panel-heading"><div><span className="eyebrow">Runtime behaviour</span><h2 id="behaviour-heading">Behaviour</h2></div></div>
|
||||
<label className="toggle-field">
|
||||
<input
|
||||
aria-label="Dry run mode"
|
||||
checked={draft.dryRun}
|
||||
onChange={(event) => onChange({ ...draft, dryRun: event.target.checked })}
|
||||
type="checkbox"
|
||||
/>
|
||||
<span><strong>Dry run mode</strong><small>Evaluate posts without sending messages.</small></span>
|
||||
</label>
|
||||
|
||||
<div className="subreddit-heading">
|
||||
<div><h3>Subreddits</h3><p>Configure monitored communities and their supported message style.</p></div>
|
||||
<button
|
||||
className="button secondary"
|
||||
onClick={() => onChange({
|
||||
...draft,
|
||||
subreddits: [
|
||||
...draft.subreddits,
|
||||
{ name: '', type: 'image', weight: '1', tone: 'default', scripted: '' },
|
||||
],
|
||||
})}
|
||||
type="button"
|
||||
>
|
||||
Add subreddit
|
||||
</button>
|
||||
</div>
|
||||
{errors.subreddits ? <p className="field-error" role="alert">{errors.subreddits}</p> : null}
|
||||
<div className="subreddit-list">
|
||||
{draft.subreddits.map((subreddit, index) => (
|
||||
<fieldset className="subreddit-row" key={index}>
|
||||
<legend>Subreddit {index + 1}</legend>
|
||||
<label className="settings-field">
|
||||
<span>Name</span>
|
||||
<input
|
||||
aria-describedby={errors[`subreddits.${index}.name`] ? `subreddit-${index}-name-error` : undefined}
|
||||
aria-invalid={errors[`subreddits.${index}.name`] ? 'true' : undefined}
|
||||
aria-label={`Subreddit ${index + 1} name`}
|
||||
onChange={(event) => onChange(updateSubreddit(draft, index, { ...subreddit, name: event.target.value }))}
|
||||
type="text"
|
||||
value={subreddit.name}
|
||||
/>
|
||||
{errors[`subreddits.${index}.name`] ? <small className="field-error" id={`subreddit-${index}-name-error`}>{errors[`subreddits.${index}.name`]}</small> : null}
|
||||
</label>
|
||||
<label className="settings-field">
|
||||
<span>Type</span>
|
||||
<select
|
||||
aria-label={`Subreddit ${index + 1} type`}
|
||||
onChange={(event) => onChange(updateSubreddit(draft, index, { ...subreddit, type: event.target.value as SubredditDraft['type'] }))}
|
||||
value={subreddit.type}
|
||||
>
|
||||
<option value="image">Image</option><option value="chat">Chat</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="settings-field">
|
||||
<span>Weight</span>
|
||||
<input
|
||||
aria-describedby={errors[`subreddits.${index}.weight`] ? `subreddit-${index}-weight-error` : undefined}
|
||||
aria-invalid={errors[`subreddits.${index}.weight`] ? 'true' : undefined}
|
||||
aria-label={`Subreddit ${index + 1} weight`}
|
||||
min="0.01"
|
||||
onChange={(event) => onChange(updateSubreddit(draft, index, { ...subreddit, weight: event.target.value }))}
|
||||
step="0.01"
|
||||
type="number"
|
||||
value={subreddit.weight}
|
||||
/>
|
||||
{errors[`subreddits.${index}.weight`] ? <small className="field-error" id={`subreddit-${index}-weight-error`}>{errors[`subreddits.${index}.weight`]}</small> : null}
|
||||
</label>
|
||||
<label className="settings-field">
|
||||
<span>Tone</span>
|
||||
<select
|
||||
aria-label={`Subreddit ${index + 1} tone`}
|
||||
onChange={(event) => onChange(updateSubreddit(draft, index, { ...subreddit, tone: event.target.value as SubredditDraft['tone'] }))}
|
||||
value={subreddit.tone}
|
||||
>
|
||||
{config.tones.map((tone) => <option key={tone} value={tone}>{tone}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label className="settings-field scripted-field">
|
||||
<span>Scripted message (optional)</span>
|
||||
<textarea
|
||||
aria-label={`Subreddit ${index + 1} scripted message`}
|
||||
onChange={(event) => onChange(updateSubreddit(draft, index, { ...subreddit, scripted: event.target.value }))}
|
||||
rows={2}
|
||||
value={subreddit.scripted}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
className="button secondary remove-subreddit"
|
||||
disabled={draft.subreddits.length === 1}
|
||||
onClick={() => onChange({
|
||||
...draft,
|
||||
subreddits: draft.subreddits.filter((_, itemIndex) => itemIndex !== index),
|
||||
})}
|
||||
type="button"
|
||||
>
|
||||
Remove subreddit {index + 1}
|
||||
</button>
|
||||
</fieldset>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user