simo преди 2 дни
родител
ревизия
404df466e2
променени са 4 файла, в които са добавени 266 реда и са изтрити 120 реда
  1. 2 0
      .gitignore
  2. 256 72
      src/components/KlaskBoard.astro
  3. 3 42
      src/pages/index.astro
  4. 5 6
      src/utils/auth.ts

+ 2 - 0
.gitignore

@@ -4,6 +4,8 @@ dist/
 # Dependencies
 node_modules/
 
+# Database (local SQLite)
+data/
 
 # Environment
 .env

+ 256 - 72
src/components/KlaskBoard.astro

@@ -3,95 +3,279 @@ interface Props {
   size?: "sm" | "md" | "lg";
 }
 const { size = "md" } = Astro.props;
+const id = `kb-${Math.random().toString(36).slice(2, 8)}`;
 ---
 
-<!-- Decorative Klask board SVG — not interactive, just visual context -->
 <svg
+  id={id}
   class={`klask-board klask-board--${size}`}
   viewBox="0 0 400 260"
   xmlns="http://www.w3.org/2000/svg"
   role="img"
   aria-label="Klask board"
 >
-  <!-- Outer board frame (wood) -->
-  <rect width="400" height="260" rx="14" fill="#c8820a"/>
-  <rect x="2" y="2" width="396" height="256" rx="13" fill="none" stroke="#e09a20" stroke-width="1.5" opacity="0.6"/>
-
-  <!-- Inner playing surface (bright yellow) -->
-  <rect x="14" y="14" width="372" height="232" rx="10" fill="#e8a40e"/>
-
-  <!-- Subtle wood grain lines -->
-  <line x1="14" y1="14" x2="386" y2="14" stroke="#d4920c" stroke-width="0.5" opacity="0.5"/>
-  <line x1="14" y1="40" x2="386" y2="40" stroke="#d4920c" stroke-width="0.5" opacity="0.3"/>
-  <line x1="14" y1="220" x2="386" y2="220" stroke="#d4920c" stroke-width="0.5" opacity="0.3"/>
-  <line x1="14" y1="246" x2="386" y2="246" stroke="#d4920c" stroke-width="0.5" opacity="0.5"/>
-
-  <!-- Center divider line -->
-  <line x1="200" y1="14" x2="200" y2="246" stroke="#d4920c" stroke-width="1" opacity="0.4"/>
-
-  <!-- Goal hole LEFT — extruded hole (raised rim + dark hole) -->
-  <!-- Outer raised rim -->
-  <circle cx="38" cy="130" r="20" fill="#b07008"/>
-  <circle cx="38" cy="130" r="17" fill="#8a5504"/>
-  <!-- The hole -->
-  <circle cx="38" cy="130" r="14" fill="#1a1008"/>
-  <circle cx="38" cy="130" r="11" fill="#0d0803"/>
-  <!-- Rim highlight -->
-  <circle cx="38" cy="130" r="20" fill="none" stroke="#e09a20" stroke-width="1" opacity="0.5"/>
-  <!-- Small shadow inside hole -->
-  <circle cx="40" cy="132" r="5" fill="#000" opacity="0.3"/>
+  <defs>
+    <linearGradient id={`sheen-${id}`} x1="0" y1="0" x2="0" y2="1">
+      <stop offset="0%" stop-color="#fff"/>
+      <stop offset="100%" stop-color="#fff" stop-opacity="0"/>
+    </linearGradient>
+    <radialGradient id={`ballglow-${id}`} cx="35%" cy="30%" r="65%">
+      <stop offset="0%"   stop-color="#fff" stop-opacity="0.9"/>
+      <stop offset="40%"  stop-color="#ffee55"/>
+      <stop offset="100%" stop-color="#e6b800"/>
+    </radialGradient>
+  </defs>
+
+  <!-- Outer board frame -->
+  <rect width="400" height="260" rx="12" fill="#1a1a2e"/>
+  <rect x="1" y="1" width="398" height="258" rx="12" fill="none" stroke="#2a2a4a" stroke-width="1.5"/>
+
+  <!-- Playing surface -->
+  <rect x="12" y="12" width="376" height="236" rx="8" fill="#1565c0"/>
+  <rect x="12" y="12" width="376" height="80" rx="8" fill={`url(#sheen-${id})`} opacity="0.12"/>
+
+  <!-- Center line -->
+  <line x1="200" y1="12" x2="200" y2="248" stroke="#fff" stroke-width="1.5" opacity="0.25"/>
+
+  <!-- Corner quarter-circles -->
+  <path d="M12,12 A28,28 0 0,1 40,12 A28,28 0 0,1 12,40 Z" fill="#fff" opacity="0.85"/>
+  <path d="M388,12 A28,28 0 0,0 360,12 A28,28 0 0,0 388,40 Z" fill="#fff" opacity="0.85"/>
+  <path d="M12,248 A28,28 0 0,0 40,248 A28,28 0 0,0 12,220 Z" fill="#fff" opacity="0.85"/>
+  <path d="M388,248 A28,28 0 0,1 360,248 A28,28 0 0,1 388,220 Z" fill="#fff" opacity="0.85"/>
+
+  <!-- KLASK watermark text -->
+  <text x="100" y="130" text-anchor="middle" dominant-baseline="middle"
+    font-family="Arial Black, sans-serif" font-weight="900" font-size="22" letter-spacing="4"
+    fill="#fff" opacity="0.18" transform="rotate(-90, 100, 130)">KLASK</text>
+  <text x="300" y="130" text-anchor="middle" dominant-baseline="middle"
+    font-family="Arial Black, sans-serif" font-weight="900" font-size="22" letter-spacing="4"
+    fill="#fff" opacity="0.18" transform="rotate(90, 300, 130)">KLASK</text>
+
+  <!-- Goal hole LEFT -->
+  <circle cx="36" cy="130" r="22" fill="#0d1b3e"/>
+  <circle cx="36" cy="130" r="18" fill="#0a1530"/>
+  <circle cx="36" cy="130" r="14" fill="#060d1e"/>
+  <circle cx="36" cy="130" r="10" fill="#020508"/>
+  <circle cx="36" cy="130" r="22" fill="none" stroke="#4a6fa5" stroke-width="2"/>
+  <circle cx="36" cy="130" r="18" fill="none" stroke="#2a4a75" stroke-width="1"/>
+  <circle cx="38" cy="132" r="5" fill="#000" opacity="0.4"/>
 
   <!-- Goal hole RIGHT -->
-  <circle cx="362" cy="130" r="20" fill="#b07008"/>
-  <circle cx="362" cy="130" r="17" fill="#8a5504"/>
-  <circle cx="362" cy="130" r="14" fill="#1a1008"/>
-  <circle cx="362" cy="130" r="11" fill="#0d0803"/>
-  <circle cx="362" cy="130" r="20" fill="none" stroke="#e09a20" stroke-width="1" opacity="0.5"/>
-  <circle cx="364" cy="132" r="5" fill="#000" opacity="0.3"/>
-
-  <!-- 3 white biscuit magnets in a row (center) -->
-  <!-- Left biscuit -->
-  <circle cx="168" cy="130" r="11" fill="#e8e0d0"/>
-  <circle cx="168" cy="130" r="8" fill="#d4ccc0"/>
-  <circle cx="168" cy="130" r="5" fill="#bab3a8"/>
-  <circle cx="166" cy="128" r="2" fill="#fff" opacity="0.4"/>
-
-  <!-- Center biscuit -->
-  <circle cx="200" cy="130" r="11" fill="#e8e0d0"/>
-  <circle cx="200" cy="130" r="8" fill="#d4ccc0"/>
-  <circle cx="200" cy="130" r="5" fill="#bab3a8"/>
-  <circle cx="198" cy="128" r="2" fill="#fff" opacity="0.4"/>
-
-  <!-- Right biscuit -->
-  <circle cx="232" cy="130" r="11" fill="#e8e0d0"/>
-  <circle cx="232" cy="130" r="8" fill="#d4ccc0"/>
-  <circle cx="232" cy="130" r="5" fill="#bab3a8"/>
-  <circle cx="230" cy="128" r="2" fill="#fff" opacity="0.4"/>
-
-  <!-- Player 1 puck (left side) — black magnetic puck -->
-  <circle cx="95" cy="130" r="22" fill="#1a1a1a"/>
-  <circle cx="95" cy="130" r="19" fill="#222"/>
-  <circle cx="95" cy="130" r="14" fill="#2a2a2a"/>
-  <!-- Puck highlight (slight shine) -->
-  <ellipse cx="90" cy="124" rx="6" ry="4" fill="#fff" opacity="0.08"/>
-  <circle cx="95" cy="130" r="22" fill="none" stroke="#3a3a3a" stroke-width="1.5"/>
-
-  <!-- Player 2 puck (right side) — black magnetic puck -->
-  <circle cx="305" cy="130" r="22" fill="#1a1a1a"/>
-  <circle cx="305" cy="130" r="19" fill="#222"/>
-  <circle cx="305" cy="130" r="14" fill="#2a2a2a"/>
-  <ellipse cx="300" cy="124" rx="6" ry="4" fill="#fff" opacity="0.08"/>
-  <circle cx="305" cy="130" r="22" fill="none" stroke="#3a3a3a" stroke-width="1.5"/>
-
-  <!-- Outer frame edge detail -->
-  <rect x="14" y="14" width="372" height="232" rx="10" fill="none" stroke="#c8820a" stroke-width="2"/>
+  <circle cx="364" cy="130" r="22" fill="#0d1b3e"/>
+  <circle cx="364" cy="130" r="18" fill="#0a1530"/>
+  <circle cx="364" cy="130" r="14" fill="#060d1e"/>
+  <circle cx="364" cy="130" r="10" fill="#020508"/>
+  <circle cx="364" cy="130" r="22" fill="none" stroke="#4a6fa5" stroke-width="2"/>
+  <circle cx="364" cy="130" r="18" fill="none" stroke="#2a4a75" stroke-width="1"/>
+  <circle cx="366" cy="132" r="5" fill="#000" opacity="0.4"/>
+
+  <!-- 3 biscuit magnets — vertical row, center -->
+  <circle cx="200" cy="88"  r="11" fill="#e8e6e0"/><circle cx="200" cy="88"  r="8" fill="#d0cdc6"/><circle cx="200" cy="88"  r="5" fill="#b8b5ae"/><circle cx="198" cy="86"  r="2" fill="#fff" opacity="0.5"/>
+  <circle cx="200" cy="130" r="11" fill="#e8e6e0"/><circle cx="200" cy="130" r="8" fill="#d0cdc6"/><circle cx="200" cy="130" r="5" fill="#b8b5ae"/><circle cx="198" cy="128" r="2" fill="#fff" opacity="0.5"/>
+  <circle cx="200" cy="172" r="11" fill="#e8e6e0"/><circle cx="200" cy="172" r="8" fill="#d0cdc6"/><circle cx="200" cy="172" r="5" fill="#b8b5ae"/><circle cx="198" cy="170" r="2" fill="#fff" opacity="0.5"/>
+
+  <!-- Ball (animated) — yellow -->
+  <g id={`${id}-ball`}>
+    <circle r="6" fill={`url(#ballglow-${id})`}/>
+    <circle r="6" fill="none" stroke="#c8960a" stroke-width="0.8" opacity="0.6"/>
+  </g>
+
+  <!-- Player 1 puck (animated) — left half, thinner: r=11 -->
+  <g id={`${id}-p1`}>
+    <circle r="11" fill="#111"/>
+    <circle r="9"  fill="#1c1c1c"/>
+    <circle r="6"  fill="#262626"/>
+    <ellipse cx="-3" cy="-3" rx="3.5" ry="2.5" fill="#fff" opacity="0.07"/>
+    <circle r="11" fill="none" stroke="#383838" stroke-width="1.2"/>
+  </g>
+
+  <!-- Player 2 puck (animated) — right half, thinner: r=11 -->
+  <g id={`${id}-p2`}>
+    <circle r="11" fill="#111"/>
+    <circle r="9"  fill="#1c1c1c"/>
+    <circle r="6"  fill="#262626"/>
+    <ellipse cx="-3" cy="-3" rx="3.5" ry="2.5" fill="#fff" opacity="0.07"/>
+    <circle r="11" fill="none" stroke="#383838" stroke-width="1.2"/>
+  </g>
+
+  <!-- Playing surface border -->
+  <rect x="12" y="12" width="376" height="236" rx="8" fill="none" stroke="#1e88e5" stroke-width="1.5" opacity="0.6"/>
 </svg>
 
+<script define:vars={{ id }}>
+(function () {
+  const svg = document.getElementById(id);
+  if (!svg) return;
+
+  const ballEl = document.getElementById(`${id}-ball`);
+  const p1El   = document.getElementById(`${id}-p1`);
+  const p2El   = document.getElementById(`${id}-p2`);
+
+  // Field bounds (playable area, inset from walls)
+  const FX1 = 24, FX2 = 376, FY1 = 24, FY2 = 236;
+  const CX = 200; // center divider x
+
+  // Static objects
+  const GOAL_L  = { x: 36,  y: 130, r: 22 };
+  const GOAL_R  = { x: 364, y: 130, r: 22 };
+  const BISCUITS = [
+    { x: 200, y: 88  },
+    { x: 200, y: 130 },
+    { x: 200, y: 172 },
+  ];
+  const BISCUIT_R = 11;
+  const PUCK_R    = 11;
+  const BALL_R    = 6;
+
+  // --- State ---
+  let ball = { x: 200, y: 130, vx: 3.2, vy: 2.1 };
+
+  // Pucks wander in their half
+  let p1 = { x: 95,  y: 130, vx: 0, vy: 0, tx: 95,  ty: 130, timer: 0 };
+  let p2 = { x: 305, y: 130, vx: 0, vy: 0, tx: 305, ty: 130, timer: 0 };
+
+  function randTarget(side) {
+    const margin = PUCK_R + 16;
+    if (side === 1) {
+      return {
+        tx: FX1 + margin + Math.random() * (CX - FX1 - margin * 2),
+        ty: FY1 + margin + Math.random() * (FY2 - FY1 - margin * 2),
+      };
+    } else {
+      return {
+        tx: CX + margin + Math.random() * (FX2 - CX - margin * 2),
+        ty: FY1 + margin + Math.random() * (FY2 - FY1 - margin * 2),
+      };
+    }
+  }
+
+  function dist(ax, ay, bx, by) {
+    const dx = ax - bx, dy = ay - by;
+    return Math.sqrt(dx * dx + dy * dy);
+  }
+
+  function resolveCircleCollision(ball, bx, by, br) {
+    const d = dist(ball.x, ball.y, bx, by);
+    const minD = BALL_R + br;
+    if (d < minD && d > 0) {
+      const nx = (ball.x - bx) / d;
+      const ny = (ball.y - by) / d;
+      const dot = ball.vx * nx + ball.vy * ny;
+      ball.vx -= 2 * dot * nx;
+      ball.vy -= 2 * dot * ny;
+      // Push ball out of overlap
+      const overlap = minD - d;
+      ball.x += nx * overlap;
+      ball.y += ny * overlap;
+      // Slight speed boost on hit so it doesn't die
+      const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
+      if (speed < 2) { ball.vx *= 2.5 / speed; ball.vy *= 2.5 / speed; }
+    }
+  }
+
+  function clampSpeed(obj, max) {
+    const s = Math.sqrt(obj.vx * obj.vx + obj.vy * obj.vy);
+    if (s > max) { obj.vx = (obj.vx / s) * max; obj.vy = (obj.vy / s) * max; }
+  }
+
+  function updatePuck(p, side, dt) {
+    p.timer -= dt;
+    if (p.timer <= 0) {
+      const t = randTarget(side);
+      p.tx = t.tx; p.ty = t.ty;
+      p.timer = 1200 + Math.random() * 1800; // ms
+    }
+    // Steer toward target
+    const dx = p.tx - p.x, dy = p.ty - p.y;
+    const d = Math.sqrt(dx * dx + dy * dy);
+    if (d > 2) {
+      p.vx += (dx / d) * 0.18;
+      p.vy += (dy / d) * 0.18;
+    }
+    // Damping
+    p.vx *= 0.88;
+    p.vy *= 0.88;
+    clampSpeed(p, 3.5);
+    p.x += p.vx;
+    p.y += p.vy;
+
+    // Clamp puck to its half
+    const margin = PUCK_R + 2;
+    if (side === 1) {
+      p.x = Math.max(FX1 + margin, Math.min(CX - margin, p.x));
+    } else {
+      p.x = Math.max(CX + margin, Math.min(FX2 - margin, p.x));
+    }
+    p.y = Math.max(FY1 + margin, Math.min(FY2 - margin, p.y));
+  }
+
+  function resetBall() {
+    ball.x = 200; ball.y = 130;
+    const angle = Math.random() * Math.PI * 2;
+    const speed = 2.8 + Math.random();
+    ball.vx = Math.cos(angle) * speed;
+    ball.vy = Math.sin(angle) * speed;
+  }
+
+  let last = performance.now();
+
+  function tick(now) {
+    const dt = Math.min(now - last, 32); // cap at 32ms
+    last = now;
+
+    // Update pucks
+    updatePuck(p1, 1, dt);
+    updatePuck(p2, 2, dt);
+
+    // Move ball
+    ball.x += ball.vx;
+    ball.y += ball.vy;
+
+    // Wall collisions
+    if (ball.x - BALL_R < FX1) { ball.x = FX1 + BALL_R; ball.vx = Math.abs(ball.vx); }
+    if (ball.x + BALL_R > FX2) { ball.x = FX2 - BALL_R; ball.vx = -Math.abs(ball.vx); }
+    if (ball.y - BALL_R < FY1) { ball.y = FY1 + BALL_R; ball.vy = Math.abs(ball.vy); }
+    if (ball.y + BALL_R > FY2) { ball.y = FY2 - BALL_R; ball.vy = -Math.abs(ball.vy); }
+
+    // Ball vs pucks
+    resolveCircleCollision(ball, p1.x, p1.y, PUCK_R);
+    resolveCircleCollision(ball, p2.x, p2.y, PUCK_R);
+
+    // Ball vs biscuits (fixed)
+    for (const b of BISCUITS) {
+      resolveCircleCollision(ball, b.x, b.y, BISCUIT_R);
+    }
+
+    // Ball into goal → reset
+    if (dist(ball.x, ball.y, GOAL_L.x, GOAL_L.y) < GOAL_L.r - 2 ||
+        dist(ball.x, ball.y, GOAL_R.x, GOAL_R.y) < GOAL_R.r - 2) {
+      resetBall();
+    }
+
+    // Cap ball speed so it doesn't go crazy
+    clampSpeed(ball, 7);
+
+    // Apply transforms
+    ballEl.setAttribute("transform", `translate(${ball.x.toFixed(1)},${ball.y.toFixed(1)})`);
+    p1El.setAttribute("transform",   `translate(${p1.x.toFixed(1)},${p1.y.toFixed(1)})`);
+    p2El.setAttribute("transform",   `translate(${p2.x.toFixed(1)},${p2.y.toFixed(1)})`);
+
+    requestAnimationFrame(tick);
+  }
+
+  // Init positions
+  resetBall();
+  p1El.setAttribute("transform", `translate(${p1.x},${p1.y})`);
+  p2El.setAttribute("transform", `translate(${p2.x},${p2.y})`);
+  requestAnimationFrame(tick);
+})();
+</script>
+
 <style>
   .klask-board {
     display: block;
     height: auto;
-    border-radius: 14px;
+    border-radius: 12px;
     box-shadow: 0 8px 32px rgba(0,0,0,0.5), 0 2px 8px rgba(0,0,0,0.3);
   }
 

+ 3 - 42
src/pages/index.astro

@@ -1,6 +1,7 @@
 ---
 import Layout from "@layouts/layout.astro";
 import Nav from "@components/Nav.astro";
+import KlaskBoard from "@components/KlaskBoard.astro";
 import { init, getLeaderboard, getRecentGames, getUser } from "@utils/db";
 import { getUserFromRequest } from "@utils/auth";
 
@@ -45,40 +46,7 @@ function timeAgo(iso: string): string {
           </div>
         </div>
         <div class="hero-board-preview">
-          <svg viewBox="0 0 360 240" xmlns="http://www.w3.org/2000/svg" class="preview-svg">
-            <rect width="360" height="240" rx="12" fill="#2a1f14"/>
-            <rect x="10" y="10" width="340" height="220" rx="8" fill="#3d2b1a"/>
-            <line x1="180" y1="10" x2="180" y2="230" stroke="#4a3525" stroke-width="1" stroke-dasharray="4 4"/>
-            <rect x="10" y="10" width="50" height="220" rx="8" fill="#2e2015" opacity="0.5"/>
-            <rect x="300" y="10" width="50" height="220" rx="8" fill="#2e2015" opacity="0.5"/>
-            <circle cx="35" cy="120" r="14" fill="#120d08"/>
-            <circle cx="35" cy="120" r="11" fill="#0a0705"/>
-            <circle cx="35" cy="120" r="8" fill="#060402"/>
-            <circle cx="325" cy="120" r="14" fill="#120d08"/>
-            <circle cx="325" cy="120" r="11" fill="#0a0705"/>
-            <circle cx="325" cy="120" r="8" fill="#060402"/>
-            <circle cx="35" cy="120" r="14" fill="none" stroke="#8a6a3a" stroke-width="1.5"/>
-            <circle cx="325" cy="120" r="14" fill="none" stroke="#8a6a3a" stroke-width="1.5"/>
-            <circle cx="165" cy="100" r="8" fill="#ddd5c8"/>
-            <circle cx="165" cy="100" r="6" fill="#c8bfb4"/>
-            <circle cx="165" cy="100" r="3" fill="#9a9590"/>
-            <circle cx="195" cy="100" r="8" fill="#ddd5c8"/>
-            <circle cx="195" cy="100" r="6" fill="#c8bfb4"/>
-            <circle cx="195" cy="100" r="3" fill="#9a9590"/>
-            <circle cx="180" cy="128" r="8" fill="#ddd5c8"/>
-            <circle cx="180" cy="128" r="6" fill="#c8bfb4"/>
-            <circle cx="180" cy="128" r="3" fill="#9a9590"/>
-            <circle cx="180" cy="112" r="6" fill="#f5c842"/>
-            <circle cx="178" cy="110" r="2" fill="#f9de80" opacity="0.6"/>
-            <circle cx="80" cy="120" r="14" fill="#3a6bc2"/>
-            <circle cx="80" cy="120" r="10" fill="#4a7fd4"/>
-            <circle cx="78" cy="118" r="4" fill="#6a9de8" opacity="0.6"/>
-            <circle cx="280" cy="120" r="14" fill="#c23a3a"/>
-            <circle cx="280" cy="120" r="10" fill="#d44a4a"/>
-            <circle cx="278" cy="118" r="4" fill="#e86a6a" opacity="0.6"/>
-            <rect x="10" y="10" width="340" height="220" rx="8" fill="none" stroke="#5a4535" stroke-width="1.5"/>
-            <rect x="0" y="0" width="360" height="240" rx="12" fill="none" stroke="#6a5040" stroke-width="1.5"/>
-          </svg>
+          <KlaskBoard size="md" />
         </div>
       </div>
     </section>
@@ -223,14 +191,7 @@ function timeAgo(iso: string): string {
     opacity: 0.85;
   }
 
-  .preview-svg {
-    width: 100%;
-    height: auto;
-    border-radius: 12px;
-    box-shadow: 0 12px 40px rgba(0,0,0,0.5);
-  }
-
-  .content-grid {
+.content-grid {
     display: grid;
     grid-template-columns: 1fr 1fr;
     gap: 24px;

+ 5 - 6
src/utils/auth.ts

@@ -1,8 +1,6 @@
 export function getUserFromRequest(request: Request): string | null {
   const cookie = request.headers.get("cookie") ?? "";
-  const match = cookie
-    .split("; ")
-    .find((c) => c.startsWith("klask_user="));
+  const match = cookie.split("; ").find((c) => c.startsWith("klask_user="));
   if (!match) return null;
   const raw = match.split("=")[1];
   return raw ? decodeURIComponent(raw) : null;
@@ -10,17 +8,18 @@ export function getUserFromRequest(request: Request): string | null {
 
 export function setUserCookie(name: string): string {
   const encoded = encodeURIComponent(name);
-  return `klask_user=${encoded}; Path=/; HttpOnly; SameSite=Lax; Max-Age=31536000`;
+  return `klask_user=${encoded}; Path=/; SameSite=Lax; Max-Age=31536000`;
 }
 
 export function clearUserCookie(): string {
-  return `klask_user=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
+  return `klask_user=; Path=/; SameSite=Lax; Max-Age=0`;
 }
 
 export function validateName(name: string): string | null {
   const trimmed = name.trim();
   if (trimmed.length < 2) return "Name must be at least 2 characters";
   if (trimmed.length > 20) return "Name must be 20 characters or less";
-  if (!/^[\w\s\-]+$/.test(trimmed)) return "Only letters, numbers, spaces, _ and - allowed";
+  if (!/^[\w\s\-]+$/.test(trimmed))
+    return "Only letters, numbers, spaces, _ and - allowed";
   return null;
 }