8881214213
Mechanical, behaviour-preserving removal of Stage N / TODO-N / phase (RN) references from comments, doc-comments, service READMEs, the current-state docs (ARCHITECTURE, FUNCTIONAL+_ru, TESTING, UI_DESIGN), config-file comments, and the .fbs/.proto schema comments. PLAN.md / PRERELEASE.md / CLAUDE.md keep the stage history. - Rename the only stage-named identifiers: registerStage8 -> registerSocialOps, registerStage11 -> registerLinkOps (gateway transcode). - Split stage6_test.go: TestEmailLoginFlow -> email_test.go, TestGuestAutoMatchLeavesNoStats (+ provisionGuest) -> account_test.go. - Regenerated proto bindings (push.pb.go, telegram_grpc.pb.go) from the de-staged .proto comments; FB Go/TS bindings unchanged (flatc strips schema comments). go build/vet/gofmt clean across modules; integration typecheck and pnpm check green.
88 lines
3.7 KiB
TypeScript
88 lines
3.7 KiB
TypeScript
// Profile-edit validation, mirroring the backend (account/profile.go,
|
|
// account/timezone.go) so the form can disable Save and flag fields before a round
|
|
// trip. Pure and unit-tested.
|
|
|
|
/** maxDisplayName caps the editable display name in runes. */
|
|
export const maxDisplayName = 32;
|
|
|
|
/** maxDisplayNameSpecials caps the total special characters (the "." / "_" separators — every
|
|
* rune that is neither a letter nor a space) a display name may carry. Mirrors the Go rule. */
|
|
export const maxDisplayNameSpecials = 5;
|
|
|
|
/** maxAwayMinutes bounds the daily away window's length (12 h). */
|
|
export const maxAwayMinutes = 12 * 60;
|
|
|
|
// Unicode letters joined by single space / "." / "_" separators, where a "." or "_"
|
|
// may be followed by a single space. No leading separator and no adjacent separators
|
|
// except "<dot|underscore> <space>"; a single trailing "." is allowed. Same
|
|
// rule as the Go displayNameRe.
|
|
const displayNameRe = /^\p{L}+(?:(?:[._] ?| )\p{L}+)*\.?$/u;
|
|
|
|
/** displayNameError returns true when the trimmed name is a valid display name. */
|
|
export function validDisplayName(raw: string): boolean {
|
|
const name = raw.trim();
|
|
const chars = [...name];
|
|
if (name.length === 0 || chars.length > maxDisplayName || !displayNameRe.test(name)) return false;
|
|
const specials = chars.filter((c) => c !== ' ' && !/\p{L}/u.test(c)).length;
|
|
return specials <= maxDisplayNameSpecials;
|
|
}
|
|
|
|
// A pragmatic email check (the backend re-validates with net/mail). Rejects spaces
|
|
// and requires a local part, an @, and a dotted domain.
|
|
const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
|
/** validEmail reports whether email is a plausible address. */
|
|
export function validEmail(email: string): boolean {
|
|
return emailRe.test(email.trim());
|
|
}
|
|
|
|
/** toMinutes parses an "HH:MM" time-of-day into minutes since midnight, or null. */
|
|
export function toMinutes(hhmm: string): number | null {
|
|
const m = /^(\d{2}):(\d{2})$/.exec(hhmm);
|
|
if (!m) return null;
|
|
const h = Number(m[1]);
|
|
const min = Number(m[2]);
|
|
if (h > 23 || min > 59) return null;
|
|
return h * 60 + min;
|
|
}
|
|
|
|
/** awayDurationOk reports whether the away window (wrapping midnight) is <= 12 h. */
|
|
export function awayDurationOk(start: string, end: string): boolean {
|
|
const s = toMinutes(start);
|
|
const e = toMinutes(end);
|
|
if (s === null || e === null) return false;
|
|
let d = e - s;
|
|
if (d < 0) d += 24 * 60;
|
|
return d <= maxAwayMinutes;
|
|
}
|
|
|
|
/** The real-world set of unique UTC offsets, for the timezone dropdown. */
|
|
export const timezoneOffsets: string[] = [
|
|
'-12:00', '-11:00', '-10:00', '-09:30', '-09:00', '-08:00', '-07:00', '-06:00',
|
|
'-05:00', '-04:00', '-03:30', '-03:00', '-02:00', '-01:00', '+00:00', '+01:00',
|
|
'+02:00', '+03:00', '+03:30', '+04:00', '+04:30', '+05:00', '+05:30', '+05:45',
|
|
'+06:00', '+06:30', '+07:00', '+08:00', '+08:45', '+09:00', '+09:30', '+10:00',
|
|
'+10:30', '+11:00', '+12:00', '+12:45', '+13:00', '+14:00',
|
|
];
|
|
|
|
/** isOffsetZone reports whether a stored timezone is a "±HH:MM" offset. */
|
|
export function isOffsetZone(tz: string): boolean {
|
|
return /^[+-]\d{2}:\d{2}$/.test(tz);
|
|
}
|
|
|
|
/** browserOffset returns the client's current UTC offset as "±HH:MM". */
|
|
export function browserOffset(): string {
|
|
const mins = -new Date().getTimezoneOffset(); // getTimezoneOffset is minutes behind UTC
|
|
const sign = mins < 0 ? '-' : '+';
|
|
const abs = Math.abs(mins);
|
|
const hh = String(Math.floor(abs / 60)).padStart(2, '0');
|
|
const mm = String(abs % 60).padStart(2, '0');
|
|
return `${sign}${hh}:${mm}`;
|
|
}
|
|
|
|
/** Hour options "00".."23" for the away-window pickers. */
|
|
export const awayHours: string[] = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'));
|
|
|
|
/** Minute options on a 10-minute step. */
|
|
export const awayMinutes: string[] = ['00', '10', '20', '30', '40', '50'];
|