File size: 4,185 Bytes
f0b240d
 
 
 
 
 
 
 
 
 
 
 
 
 
7e7e06a
0b2fee5
 
 
 
 
 
 
 
 
 
 
7e7e06a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e535f52
 
 
 
7e7e06a
 
 
 
 
 
 
e535f52
 
 
 
7e7e06a
 
e535f52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7e7e06a
 
 
e535f52
 
 
7e7e06a
 
 
e535f52
 
 
 
7e7e06a
e535f52
7e7e06a
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
export function toxicityColor(probability: number): string {
  const pct = probability * 100;
  if (pct >= 70) return "#ff4e45";
  if (pct >= 40) return "#f5a623";
  return "#2ba640";
}

export function formatPct(probability: number): string {
  return `${Math.round(probability * 100)}%`;
}

export function newId(): string {
  return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
}

export const TEAM_MEMBERS = [
  "Roberto Molero",
  "Mirae Kang",
  "Jonathan Brasales",
  "Andrés Torrez",
] as const;

export function randomTeamMember(): string {
  return TEAM_MEMBERS[Math.floor(Math.random() * TEAM_MEMBERS.length)];
}

const RTF = new Intl.RelativeTimeFormat("es", { numeric: "auto" });

const TIME_UNITS: Array<[Intl.RelativeTimeFormatUnit, number]> = [
  ["year", 60 * 60 * 24 * 365],
  ["month", 60 * 60 * 24 * 30],
  ["day", 60 * 60 * 24],
  ["hour", 60 * 60],
  ["minute", 60],
  ["second", 1],
];

export function relativeTime(input: string | number | Date): string {
  const date = input instanceof Date ? input : new Date(input);
  const seconds = (date.getTime() - Date.now()) / 1000;
  if (!Number.isFinite(seconds)) return "";
  for (const [unit, secondsInUnit] of TIME_UNITS) {
    if (Math.abs(seconds) >= secondsInUnit || unit === "second") {
      return RTF.format(Math.round(seconds / secondsInUnit), unit);
    }
  }
  return "";
}

export function truncate(text: string, max = 140): string {
  if (text.length <= max) return text;
  return `${text.slice(0, max - 1).trimEnd()}…`;
}

const ADJECTIVES = [
  "happy", "lucky", "wild", "calm", "bright", "swift", "quiet", "lazy",
  "brave", "fuzzy", "shiny", "sleepy", "quick", "noble", "loud", "humble",
  "stormy", "sunny", "rainy", "frosty", "spicy", "salty", "sweet", "crazy",
  "mighty", "silent", "epic", "cosmic", "rusty", "neon", "wandering", "lone",
  "electric", "atomic", "midnight", "golden", "silver", "crimson", "violet", "azure",
  "ancient", "feral", "tidal", "lunar", "solar", "ember", "frost", "iron",
  "velvet", "wicked", "groovy", "funky", "chill", "vibrant", "moody", "savage",
  "tropical", "alpine", "urban", "rogue", "stellar", "nomadic", "boreal", "obsidian",
];

const NOUNS = [
  "panda", "tiger", "falcon", "otter", "eagle", "wolf", "fox", "bear",
  "lynx", "shark", "raven", "viper", "phoenix", "dragon", "jaguar", "koala",
  "moose", "lion", "owl", "octopus", "rabbit", "hawk", "badger", "robin",
  "cosmonaut", "drifter", "ninja", "wizard", "rider", "pilot", "skater", "gamer",
  "barista", "hacker", "painter", "poet", "sailor", "ranger", "archer", "dancer",
  "chef", "cyclist", "writer", "drummer", "diver", "climber", "biker", "florist",
  "comet", "nebula", "nova", "pulsar", "quasar", "asteroid", "meteor", "orbiter",
  "knight", "monk", "scribe", "merchant", "wanderer", "voyager", "explorer", "pirate",
];

const SUFFIXES = [
  "", "", "", "", "",
  "_", "_x", "_yt", "_hd", "_real", "_official", "_tv", "_live", "_xd", "_v2",
  "99", "01", "07", "23", "42",
  "_irl", "_pro",
];

function hashString(str: string): number {
  let h = 0x811c9dc5;
  for (let i = 0; i < str.length; i++) {
    h ^= str.charCodeAt(i);
    h = Math.imul(h, 0x01000193) >>> 0;
  }
  return h >>> 0;
}

function mix(h: number, salt: number): number {
  let x = (h ^ Math.imul(salt + 1, 0x85ebca6b)) >>> 0;
  x = Math.imul(x ^ (x >>> 13), 0xc2b2ae35) >>> 0;
  return (x ^ (x >>> 16)) >>> 0;
}

/**
 * Generate a YouTube-style random username, deterministic per seed.
 * Three independent hash mixes pick adjective, noun, and suffix so that
 * seeds that share a prefix (e.g. `yt-VIDEOID-0`, `yt-VIDEOID-1`) still
 * produce visibly different usernames.
 */
export function randomUsername(seed: string | number | undefined | null): string {
  const str = seed == null ? "anon" : String(seed);
  const base = hashString(str);
  const adj = ADJECTIVES[mix(base, 1) % ADJECTIVES.length] ?? "anon";
  const noun = NOUNS[mix(base, 2) % NOUNS.length] ?? "user";
  const suffix = SUFFIXES[mix(base, 3) % SUFFIXES.length] ?? "";
  const needsNum = suffix === "" || suffix.endsWith("_");
  const num = needsNum ? String(mix(base, 4) % 1000) : "";
  return `${adj}_${noun}${suffix}${num}`;
}