Skip to content

Commit f4d843f

Browse files
committed
working on profile screen
1 parent 443cad5 commit f4d843f

12 files changed

Lines changed: 543 additions & 257 deletions

File tree

AGENTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
When the user changes subject or goes on a side tangant about something, assume first that they are making a comment and respond, rather than being too quick to edit files related to the side tangent before they tell you what they want.
2+
3+
## Workspace instructions
4+
5+
- Prefer the following tools: get_errors, vscode_listCodeUsages, vscode_renameSymbol, lsp_code_actions, lsp_completion, lsp_definition, lsp_document_symbols, lsp_format_document, lsp_hover, lsp_references, lsp_rename_symbol, lsp_signature_help, lsp_workspace_symbols.
6+
7+
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { JSXElement } from "../../jsx-lit/src/JSXElement";
2+
import { AdminRecord, PerTabFieldState, DraftChangeHandler, PendingRowsChangeHandler, PermissionRowsChangeHandler, ResolverTitleChangeHandler, OperationTriggerHandler } from "./app";
3+
import { renderFieldEditor } from "./definition/renders";
4+
import { getEmptyItems } from "./definition/store";
5+
import { PermissionRow, FieldDefinition } from "./definition/tabs";
6+
import { requestUpdate } from "./requestUpdate";
7+
8+
9+
export class FomController<T extends AdminRecord> {
10+
11+
12+
constructor(public context: JSXElement & { draft: T }) { }
13+
14+
private get draft(): T { return this.context.draft }
15+
16+
private readonly itemsByTab = getEmptyItems();
17+
18+
requestUpdate() { this.context.requestUpdate(); }
19+
20+
@requestUpdate private accessor isSubmitting: boolean = false;
21+
@requestUpdate private accessor submitMessage: string = new URLSearchParams(globalThis.location?.search ?? "").get("state") === "password-changed"
22+
? "Password updated. You can now log in."
23+
: "";
24+
@requestUpdate private accessor operationMessages: Record<string, string> = {};
25+
@requestUpdate private accessor pendingRows: Record<string, number> = {};
26+
@requestUpdate private accessor transientPermissionRows: Record<string, PermissionRow[]> = {};
27+
28+
private get fieldState(): PerTabFieldState {
29+
return {
30+
tabId: "users",
31+
mode: "create",
32+
draft: this.draft,
33+
saved: this.draft,
34+
resolverTitle: "",
35+
operationMessages: this.operationMessages,
36+
pendingRows: this.pendingRows,
37+
transientPermissionRows: this.transientPermissionRows,
38+
};
39+
}
40+
41+
// #region context
42+
private readonly updateDraft: DraftChangeHandler = (fieldKey, value) => {
43+
(this.draft as unknown as Record<string, unknown>)[fieldKey] = value;
44+
this.requestUpdate();
45+
};
46+
47+
private readonly updatePendingRows: PendingRowsChangeHandler = (fieldKey, updater) => {
48+
this.pendingRows[fieldKey] = Math.max(0, updater(this.pendingRows[fieldKey] ?? 0));
49+
this.requestUpdate();
50+
};
51+
52+
private readonly updateTransientPermissionRows: PermissionRowsChangeHandler = (fieldKey, rows) => {
53+
this.transientPermissionRows[fieldKey] = rows;
54+
this.requestUpdate();
55+
};
56+
57+
private readonly updateResolverTitle: ResolverTitleChangeHandler = () => {
58+
this.requestUpdate();
59+
};
60+
61+
private readonly triggerOperation: OperationTriggerHandler = (fieldKey, message) => {
62+
this.operationMessages[fieldKey] = message;
63+
this.requestUpdate();
64+
};
65+
66+
67+
// #region handleAny
68+
readonly handleAnySubmit = async (message: string, failed: string, cb: () => Promise<boolean>) => {
69+
this.isSubmitting = true;
70+
this.submitMessage = message;
71+
try {
72+
if (!await cb()) {
73+
this.submitMessage = failed;
74+
}
75+
} catch (error) {
76+
console.error(error);
77+
if (error instanceof Error) {
78+
try {
79+
const se = JSON.parse(error.message);
80+
if (se.reason && se.status) {
81+
this.submitMessage = se.reason;
82+
if (se.details) {
83+
Object.entries(se.details).forEach(([k, v]) => {
84+
this.submitMessage += "\n" + k + ": " + v;
85+
});
86+
}
87+
return;
88+
}
89+
} catch (e) {
90+
}
91+
}
92+
this.submitMessage = error instanceof Error ? error.message : failed;
93+
} finally {
94+
this.isSubmitting = false;
95+
}
96+
};
97+
98+
// #region field
99+
renderField(field: FieldDefinition): JSX.Node {
100+
const value = (this.draft as any)[field.key] ?? "";
101+
const inputId = `login-${field.key}`;
102+
103+
return (
104+
<div class="login-field">
105+
<label class="login-field-label" for={inputId}>{field.label}</label>
106+
{renderFieldEditor({
107+
field: field as unknown as FieldDefinition,
108+
value,
109+
disabled: this.isSubmitting,
110+
fieldState: this.fieldState,
111+
itemsByTab: this.itemsByTab,
112+
onDraftChange: this.updateDraft,
113+
onPendingRowsChange: this.updatePendingRows,
114+
onTransientPermissionRowsChange: this.updateTransientPermissionRows,
115+
onResolverTitleChange: this.updateResolverTitle,
116+
onTriggerOperation: this.triggerOperation,
117+
inputId,
118+
})}
119+
</div>
120+
);
121+
}
122+
// #region common
123+
// Keep the shared card structure in one place so each form mode only supplies its
124+
// mode-specific title, copy, fields, extra content, and actions.
125+
renderCommon({ title, copy, submitDisabled, submitAction, submitLabel, isStart: isStart, handleBackClick }: {
126+
title: string;
127+
copy: string;
128+
submitDisabled: boolean;
129+
submitLabel: string;
130+
submitAction: () => Promise<void>;
131+
handleBackClick: () => Promise<void>;
132+
isStart?: boolean;
133+
}, content: JSX.Node): JSX.Node {
134+
return (
135+
<div class="admin-shell">
136+
<section class="modal-card" aria-label="Login form" style="max-width: 30rem; margin: 0 auto; width: 100%;">
137+
<header class="login-card-header">
138+
<div class="login-card-title">
139+
<h3>{title}</h3>
140+
<p class="login-card-copy">{copy}</p>
141+
{/* {this.serverState ? <p class="login-card-meta">Server state ready.</p> : null} */}
142+
</div>
143+
</header>
144+
145+
<div class="login-card-body">
146+
{content}
147+
148+
{this.submitMessage ? (
149+
<div class="login-feedback" role="status" aria-live="polite">
150+
{this.submitMessage.split("\n").map(e => <p>{e}</p>)}
151+
</div>
152+
) : null}
153+
154+
{isStart ? (
155+
<button
156+
class="primary-button login-submit"
157+
type="button"
158+
disabled={submitDisabled}
159+
onclick={submitAction}
160+
>{submitLabel}</button>
161+
) : (
162+
<footer class="login-actions">
163+
<div class="login-action-row">
164+
{!isStart && <button class="ghost-button"
165+
type="button"
166+
disabled={this.isSubmitting}
167+
onclick={handleBackClick}
168+
>Cancel</button>}
169+
<button
170+
class="primary-button login-submit"
171+
type="button"
172+
disabled={submitDisabled}
173+
onclick={submitAction}
174+
>{submitLabel}</button>
175+
</div>
176+
</footer>
177+
)}
178+
179+
</div>
180+
</section>
181+
</div>
182+
);
183+
}
184+
185+
}

0 commit comments

Comments
 (0)