Add Prettier for TypeScript formatting
- Install prettier - Configure .prettierrc.json and .prettierignore - Add npm scripts: format, format:check - Add Makefile target: format-frontend - Format all frontend files
This commit is contained in:
parent
4b394b0698
commit
37de6f70e0
44 changed files with 906 additions and 856 deletions
|
|
@ -162,12 +162,13 @@ describe("ProfilePage - Display", () => {
|
|||
test("displays empty fields when profile has null values", async () => {
|
||||
vi.spyOn(global, "fetch").mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
contact_email: null,
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
contact_email: null,
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
} as Response);
|
||||
|
||||
render(<ProfilePage />);
|
||||
|
|
@ -291,7 +292,7 @@ describe("ProfilePage - Form Behavior", () => {
|
|||
} as Response);
|
||||
|
||||
render(<ProfilePage />);
|
||||
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue("@testuser")).toBeDefined();
|
||||
});
|
||||
|
|
@ -308,78 +309,83 @@ describe("ProfilePage - Form Behavior", () => {
|
|||
test("auto-prepends @ to telegram when user starts with letter", async () => {
|
||||
vi.spyOn(global, "fetch").mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
contact_email: null,
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
contact_email: null,
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
} as Response);
|
||||
|
||||
render(<ProfilePage />);
|
||||
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
|
||||
});
|
||||
|
||||
const telegramInput = document.getElementById("telegram") as HTMLInputElement;
|
||||
|
||||
|
||||
// Type a letter without @ - should auto-prepend @
|
||||
fireEvent.change(telegramInput, { target: { value: "myhandle" } });
|
||||
|
||||
|
||||
expect(telegramInput.value).toBe("@myhandle");
|
||||
});
|
||||
|
||||
test("does not auto-prepend @ if user types @ first", async () => {
|
||||
vi.spyOn(global, "fetch").mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
contact_email: null,
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
contact_email: null,
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
} as Response);
|
||||
|
||||
render(<ProfilePage />);
|
||||
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
|
||||
});
|
||||
|
||||
const telegramInput = document.getElementById("telegram") as HTMLInputElement;
|
||||
|
||||
|
||||
// User types @ first - no auto-prepend
|
||||
fireEvent.change(telegramInput, { target: { value: "@myhandle" } });
|
||||
|
||||
|
||||
expect(telegramInput.value).toBe("@myhandle");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ProfilePage - Form Submission", () => {
|
||||
test("shows success toast after successful save", async () => {
|
||||
const fetchSpy = vi.spyOn(global, "fetch")
|
||||
const fetchSpy = vi
|
||||
.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
contact_email: null,
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
contact_email: null,
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
contact_email: "new@example.com",
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
contact_email: "new@example.com",
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
} as Response);
|
||||
|
||||
render(<ProfilePage />);
|
||||
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
|
||||
});
|
||||
|
|
@ -410,27 +416,29 @@ describe("ProfilePage - Form Submission", () => {
|
|||
vi.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
contact_email: null,
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
contact_email: null,
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 422,
|
||||
json: () => Promise.resolve({
|
||||
detail: {
|
||||
field_errors: {
|
||||
telegram: "Backend error: invalid handle",
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
detail: {
|
||||
field_errors: {
|
||||
telegram: "Backend error: invalid handle",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
} as Response);
|
||||
|
||||
render(<ProfilePage />);
|
||||
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
|
||||
});
|
||||
|
|
@ -452,17 +460,18 @@ describe("ProfilePage - Form Submission", () => {
|
|||
vi.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
contact_email: null,
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
contact_email: null,
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
} as Response)
|
||||
.mockRejectedValueOnce(new Error("Network error"));
|
||||
|
||||
render(<ProfilePage />);
|
||||
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
|
||||
});
|
||||
|
|
@ -489,17 +498,18 @@ describe("ProfilePage - Form Submission", () => {
|
|||
vi.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
contact_email: null,
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
contact_email: null,
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
} as Response)
|
||||
.mockReturnValueOnce(submitPromise as Promise<Response>);
|
||||
|
||||
render(<ProfilePage />);
|
||||
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
|
||||
});
|
||||
|
|
@ -519,12 +529,13 @@ describe("ProfilePage - Form Submission", () => {
|
|||
// Resolve the promise
|
||||
resolveSubmit!({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
contact_email: "new@example.com",
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
contact_email: "new@example.com",
|
||||
telegram: null,
|
||||
signal: null,
|
||||
nostr_npub: null,
|
||||
}),
|
||||
} as Response);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -532,4 +543,3 @@ describe("ProfilePage - Form Submission", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ function validateEmail(value: string): string | undefined {
|
|||
if (!value) return undefined;
|
||||
// More comprehensive email regex that matches email-validator behavior
|
||||
// Checks for: local part, @, domain with at least one dot, valid TLD
|
||||
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
|
||||
const emailRegex =
|
||||
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
|
||||
if (!emailRegex.test(value)) {
|
||||
return "Please enter a valid email address";
|
||||
}
|
||||
|
|
@ -72,7 +73,7 @@ function validateNostrNpub(value: string): string | undefined {
|
|||
if (!value.startsWith(npubRules.prefix)) {
|
||||
return `Nostr npub must start with '${npubRules.prefix}'`;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const decoded = bech32.decode(value);
|
||||
if (decoded.prefix !== "npub") {
|
||||
|
|
@ -186,7 +187,7 @@ export default function ProfilePage() {
|
|||
|
||||
const handleInputChange = (field: keyof FormData) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let value = e.target.value;
|
||||
|
||||
|
||||
// For telegram: auto-prepend @ if user starts with a valid letter
|
||||
if (field === "telegram" && value && !value.startsWith("@")) {
|
||||
// Check if first char is a valid telegram handle start (letter)
|
||||
|
|
@ -194,14 +195,14 @@ export default function ProfilePage() {
|
|||
value = "@" + value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
|
||||
|
||||
// Clear any pending validation timeout
|
||||
if (validationTimeoutRef.current) {
|
||||
clearTimeout(validationTimeoutRef.current);
|
||||
}
|
||||
|
||||
|
||||
// Debounce validation - wait 500ms after user stops typing
|
||||
validationTimeoutRef.current = setTimeout(() => {
|
||||
const newFormData = { ...formData, [field]: value };
|
||||
|
|
@ -212,11 +213,11 @@ export default function ProfilePage() {
|
|||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
// Validate all fields
|
||||
const validationErrors = validateForm(formData);
|
||||
setErrors(validationErrors);
|
||||
|
||||
|
||||
if (Object.keys(validationErrors).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -300,9 +301,7 @@ export default function ProfilePage() {
|
|||
style={{ ...styles.input, ...styles.inputReadOnly }}
|
||||
disabled
|
||||
/>
|
||||
<span style={styles.hint}>
|
||||
This is your login email and cannot be changed here.
|
||||
</span>
|
||||
<span style={styles.hint}>This is your login email and cannot be changed here.</span>
|
||||
</div>
|
||||
|
||||
{/* Godfather - shown if user was invited */}
|
||||
|
|
@ -315,9 +314,7 @@ export default function ProfilePage() {
|
|||
<div style={styles.godfatherBox}>
|
||||
<span style={styles.godfatherEmail}>{godfatherEmail}</span>
|
||||
</div>
|
||||
<span style={styles.hint}>
|
||||
The user who invited you to join.
|
||||
</span>
|
||||
<span style={styles.hint}>The user who invited you to join.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -344,9 +341,7 @@ export default function ProfilePage() {
|
|||
}}
|
||||
placeholder="alternate@example.com"
|
||||
/>
|
||||
{errors.contact_email && (
|
||||
<span style={styles.errorText}>{errors.contact_email}</span>
|
||||
)}
|
||||
{errors.contact_email && <span style={styles.errorText}>{errors.contact_email}</span>}
|
||||
</div>
|
||||
|
||||
{/* Telegram */}
|
||||
|
|
@ -365,9 +360,7 @@ export default function ProfilePage() {
|
|||
}}
|
||||
placeholder="@username"
|
||||
/>
|
||||
{errors.telegram && (
|
||||
<span style={styles.errorText}>{errors.telegram}</span>
|
||||
)}
|
||||
{errors.telegram && <span style={styles.errorText}>{errors.telegram}</span>}
|
||||
</div>
|
||||
|
||||
{/* Signal */}
|
||||
|
|
@ -386,9 +379,7 @@ export default function ProfilePage() {
|
|||
}}
|
||||
placeholder="username.01"
|
||||
/>
|
||||
{errors.signal && (
|
||||
<span style={styles.errorText}>{errors.signal}</span>
|
||||
)}
|
||||
{errors.signal && <span style={styles.errorText}>{errors.signal}</span>}
|
||||
</div>
|
||||
|
||||
{/* Nostr npub */}
|
||||
|
|
@ -407,9 +398,7 @@ export default function ProfilePage() {
|
|||
}}
|
||||
placeholder="npub1..."
|
||||
/>
|
||||
{errors.nostr_npub && (
|
||||
<span style={styles.errorText}>{errors.nostr_npub}</span>
|
||||
)}
|
||||
{errors.nostr_npub && <span style={styles.errorText}>{errors.nostr_npub}</span>}
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue