Formatting and cleanup
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { auth } from "../api";
|
||||
import type { UserInfo } from "../types";
|
||||
import "./AccountPanel.css";
|
||||
import { FaDiscord } from "react-icons/fa6";
|
||||
|
||||
interface Props {
|
||||
user: UserInfo;
|
||||
@@ -43,7 +44,7 @@ export default function AccountPanel({ user, onClose, onRefresh }: Props) {
|
||||
setProfileSuccess(false);
|
||||
}
|
||||
|
||||
async function handleSaveProfile(e: React.SubmitEvent<HTMLFormElement>) {
|
||||
async function handleSaveProfile(e: React.SubmitEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
setProfileSaving(true);
|
||||
setProfileError(null);
|
||||
@@ -63,7 +64,7 @@ export default function AccountPanel({ user, onClose, onRefresh }: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleChangePassword(e: React.FormEvent) {
|
||||
async function handleChangePassword(e: React.SubmitEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
setPwError(null);
|
||||
setPwSuccess(false);
|
||||
@@ -72,10 +73,6 @@ export default function AccountPanel({ user, onClose, onRefresh }: Props) {
|
||||
setPwError("Passwords do not match");
|
||||
return;
|
||||
}
|
||||
if (pwNew.length < 8) {
|
||||
setPwError("Password must be at least 8 characters");
|
||||
return;
|
||||
}
|
||||
|
||||
setPwSaving(true);
|
||||
try {
|
||||
@@ -296,13 +293,7 @@ export default function AccountPanel({ user, onClose, onRefresh }: Props) {
|
||||
<h3>Connected Accounts</h3>
|
||||
|
||||
<div className="account-connection">
|
||||
<svg
|
||||
className="connection-icon discord-icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z" />
|
||||
</svg>
|
||||
<FaDiscord />
|
||||
|
||||
<div className="connection-info">
|
||||
<span className="connection-name">Discord</span>
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function FloatingMapControls({
|
||||
return (
|
||||
<div className="floating-map-controls">
|
||||
{/* Always visible for logged-in users */}
|
||||
<button className="fmc-btn" onClick={onViewMaps} title="View my maps">
|
||||
<button className="fmc-btn" onClick={onViewMaps} title="View maps">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
|
||||
@@ -17,10 +17,6 @@ import { useWebSocket } from "../hooks/useWebSocket";
|
||||
import TokenDialog from "./TokenDialog";
|
||||
import "./Grid.css";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const DEFAULT_ZOOM = 40;
|
||||
const MIN_ZOOM = 8;
|
||||
const MAX_ZOOM = 160;
|
||||
@@ -36,10 +32,6 @@ const MAX_FLOOD_CELLS = 2500;
|
||||
/** World units per second for WASD keyboard panning. */
|
||||
const WASD_PAN_SPEED = 12;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface Camera {
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
@@ -58,10 +50,6 @@ export interface GridHandle {
|
||||
sendColorUpdate: (colors: string[]) => void;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function cellKey(x: number, y: number): string {
|
||||
return `${x},${y}`;
|
||||
}
|
||||
@@ -265,10 +253,6 @@ function clampCameraToContent(
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Grid component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Grid = forwardRef<GridHandle, Props>(function Grid(
|
||||
{ mapId, tool, paintColor, tokenColor, onColorsLoaded },
|
||||
ref,
|
||||
@@ -316,18 +300,12 @@ const Grid = forwardRef<GridHandle, Props>(function Grid(
|
||||
null,
|
||||
);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Imperative handle — lets App.tsx trigger a color WS update
|
||||
// -------------------------------------------------------------------------
|
||||
useImperativeHandle(ref, () => ({
|
||||
sendColorUpdate(colors: string[]) {
|
||||
sendRef.current({ type: "update_colors", colors });
|
||||
},
|
||||
}));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Resize canvas to fill container
|
||||
// -------------------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
@@ -345,9 +323,6 @@ const Grid = forwardRef<GridHandle, Props>(function Grid(
|
||||
return () => observer.disconnect();
|
||||
}, [redraw]);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// WebSocket
|
||||
// -------------------------------------------------------------------------
|
||||
// Keep a stable ref to the callback so handleMessage doesn't re-create
|
||||
const onColorsLoadedRef = useRef(onColorsLoaded);
|
||||
useEffect(() => {
|
||||
@@ -437,9 +412,6 @@ const Grid = forwardRef<GridHandle, Props>(function Grid(
|
||||
sendRef.current = send;
|
||||
}, [send]);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Canvas draw
|
||||
// -------------------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
@@ -518,9 +490,6 @@ const Grid = forwardRef<GridHandle, Props>(function Grid(
|
||||
}
|
||||
}, [tick]);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Camera helpers
|
||||
// -------------------------------------------------------------------------
|
||||
function applyClampAndRedraw() {
|
||||
const canvas = canvasRef.current;
|
||||
if (canvas) {
|
||||
@@ -546,9 +515,6 @@ const Grid = forwardRef<GridHandle, Props>(function Grid(
|
||||
applyClampAndRedraw();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Wheel → zoom
|
||||
// -------------------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
@@ -564,9 +530,6 @@ const Grid = forwardRef<GridHandle, Props>(function Grid(
|
||||
return () => canvas.removeEventListener("wheel", onWheel);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// WASD panning — requestAnimationFrame loop
|
||||
// -------------------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
function rafTick(timestamp: number) {
|
||||
const keys = keysHeld.current;
|
||||
@@ -640,9 +603,6 @@ const Grid = forwardRef<GridHandle, Props>(function Grid(
|
||||
};
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Mouse helpers
|
||||
// -------------------------------------------------------------------------
|
||||
function getCanvasPoint(e: React.MouseEvent) {
|
||||
const rect = canvasRef.current!.getBoundingClientRect();
|
||||
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
||||
@@ -655,9 +615,6 @@ const Grid = forwardRef<GridHandle, Props>(function Grid(
|
||||
return null;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Mouse handlers
|
||||
// -------------------------------------------------------------------------
|
||||
function handleMouseDown(e: React.MouseEvent) {
|
||||
e.preventDefault();
|
||||
const { x: mx, y: my } = getCanvasPoint(e);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { auth } from "../api";
|
||||
import type { UserInfo } from "../types";
|
||||
import "./LoginModal.css";
|
||||
import { FaDiscord } from "react-icons/fa6";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
@@ -103,7 +104,7 @@ export default function LoginModal({ onClose, onLogin }: Props) {
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoComplete="username"
|
||||
required
|
||||
minLength={1}
|
||||
minLength={3}
|
||||
maxLength={32}
|
||||
/>
|
||||
</label>
|
||||
@@ -117,7 +118,6 @@ export default function LoginModal({ onClose, onLogin }: Props) {
|
||||
tab === "login" ? "current-password" : "new-password"
|
||||
}
|
||||
required
|
||||
minLength={8}
|
||||
/>
|
||||
</label>
|
||||
{tab === "register" && (
|
||||
@@ -129,7 +129,6 @@ export default function LoginModal({ onClose, onLogin }: Props) {
|
||||
onChange={(e) => setConfirm(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
minLength={8}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
@@ -151,9 +150,7 @@ export default function LoginModal({ onClose, onLogin }: Props) {
|
||||
|
||||
{/* Discord OAuth */}
|
||||
<button className="btn-discord" onClick={handleDiscordLogin}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z" />
|
||||
</svg>
|
||||
<FaDiscord />
|
||||
Log In with Discord
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -108,6 +108,12 @@
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.map-list-updated {
|
||||
font-size: 0.68rem;
|
||||
color: #4b5563;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Role badge reused from EditMapModal */
|
||||
.perm-role-badge {
|
||||
font-size: 0.7rem;
|
||||
|
||||
@@ -2,6 +2,21 @@ import { useState } from "react";
|
||||
import { api } from "../api";
|
||||
import type { ListedMap } from "../types";
|
||||
import "./MapListModal.css";
|
||||
import { FaTrash } from "react-icons/fa6";
|
||||
|
||||
function timeAgo(date: Date): string {
|
||||
const diff = Date.now() - new Date(date).getTime();
|
||||
const minutes = Math.floor(diff / 60_000);
|
||||
if (minutes < 1) return "just now";
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
const days = Math.floor(hours / 24);
|
||||
if (days < 30) return `${days}d ago`;
|
||||
const months = Math.floor(days / 30);
|
||||
if (months < 12) return `${months}mo ago`;
|
||||
return `${Math.floor(months / 12)}y ago`;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
maps: ListedMap[];
|
||||
@@ -83,7 +98,7 @@ export default function MapListModal({
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="modal-header">
|
||||
<h2>My Maps</h2>
|
||||
<h2>Maps</h2>
|
||||
<button className="modal-close" onClick={onClose} aria-label="Close">
|
||||
✕
|
||||
</button>
|
||||
@@ -95,86 +110,123 @@ export default function MapListModal({
|
||||
</p>
|
||||
) : (
|
||||
<div className="map-list-scroll">
|
||||
{maps.map((map) => (
|
||||
<div
|
||||
key={map.id}
|
||||
className={`map-list-row ${map.id === selectedMapId ? "active" : ""}`}
|
||||
onClick={() => handleSelect(map)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSelect(map)}
|
||||
>
|
||||
<div className="map-list-main">
|
||||
<span className="map-list-name">{map.name}</span>
|
||||
<div className="map-list-meta">
|
||||
<span className="map-list-owner">
|
||||
by {map.owner_username}
|
||||
</span>
|
||||
<span
|
||||
className={`map-access-badge access-${map.public_access}`}
|
||||
>
|
||||
{accessLabel(map.public_access)}
|
||||
</span>
|
||||
{map.user_role && (
|
||||
<span className={`perm-role-badge role-${map.user_role}`}>
|
||||
{map.user_role}
|
||||
{[...maps]
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.updated_at).getTime() -
|
||||
new Date(a.updated_at).getTime(),
|
||||
)
|
||||
.map((map) => (
|
||||
<div
|
||||
key={map.id}
|
||||
className={`map-list-row ${map.id === selectedMapId ? "active" : ""}`}
|
||||
onClick={() => handleSelect(map)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSelect(map)}
|
||||
>
|
||||
<div className="map-list-main">
|
||||
<span className="map-list-name">{map.name}</span>
|
||||
<div className="map-list-meta">
|
||||
<span className="map-list-owner">
|
||||
by {map.owner_username}
|
||||
</span>
|
||||
)}
|
||||
{map.is_favorited && !map.user_role && (
|
||||
<span className="map-fav-badge">★ Favorited</span>
|
||||
)}
|
||||
<span
|
||||
className={`map-access-badge access-${map.public_access}`}
|
||||
>
|
||||
{accessLabel(map.public_access)}
|
||||
</span>
|
||||
{map.user_role && (
|
||||
<span
|
||||
className={`perm-role-badge role-${map.user_role}`}
|
||||
>
|
||||
{map.user_role}
|
||||
</span>
|
||||
)}
|
||||
{map.is_favorited && !map.user_role && (
|
||||
<span className="map-fav-badge">★ Favorited</span>
|
||||
)}
|
||||
<span
|
||||
className="map-list-updated"
|
||||
title={new Date(map.updated_at).toLocaleString()}
|
||||
>
|
||||
{timeAgo(map.updated_at)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="map-list-actions">
|
||||
{/* Favorite toggle */}
|
||||
<button
|
||||
className={`map-action-btn fav-btn ${map.is_favorited ? "fav-active" : ""}`}
|
||||
onClick={(e) => handleFavoriteToggle(e, map)}
|
||||
disabled={togglingId === map.id}
|
||||
title={
|
||||
map.is_favorited
|
||||
? "Remove from favorites"
|
||||
: "Add to favorites"
|
||||
}
|
||||
>
|
||||
{map.is_favorited ? "★" : "☆"}
|
||||
</button>
|
||||
|
||||
{/* Copy link */}
|
||||
<button
|
||||
className="map-action-btn copy-btn"
|
||||
onClick={(e) => handleCopyLink(e, map)}
|
||||
title="Copy link"
|
||||
>
|
||||
{copiedId === map.id ? (
|
||||
<span className="copied-text">✓</span>
|
||||
) : (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect
|
||||
x="9"
|
||||
y="9"
|
||||
width="13"
|
||||
height="13"
|
||||
rx="2"
|
||||
ry="2"
|
||||
/>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Delete map button */}
|
||||
<button
|
||||
className="map-action-btn delete-btn"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (
|
||||
window.confirm(
|
||||
"Are you sure you want to delete this map? This action cannot be undone.",
|
||||
)
|
||||
) {
|
||||
try {
|
||||
await api.deleteMap(map.id);
|
||||
onMapsChange(maps.filter((m) => m.id !== map.id));
|
||||
} catch (err) {
|
||||
console.error("Failed to delete map", err);
|
||||
}
|
||||
}
|
||||
}}
|
||||
title="Delete map"
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="map-list-actions">
|
||||
{/* Favorite toggle */}
|
||||
<button
|
||||
className={`map-action-btn fav-btn ${map.is_favorited ? "fav-active" : ""}`}
|
||||
onClick={(e) => handleFavoriteToggle(e, map)}
|
||||
disabled={togglingId === map.id}
|
||||
title={
|
||||
map.is_favorited
|
||||
? "Remove from favorites"
|
||||
: "Add to favorites"
|
||||
}
|
||||
>
|
||||
{map.is_favorited ? "★" : "☆"}
|
||||
</button>
|
||||
|
||||
{/* Copy link */}
|
||||
<button
|
||||
className="map-action-btn copy-btn"
|
||||
onClick={(e) => handleCopyLink(e, map)}
|
||||
title="Copy link"
|
||||
>
|
||||
{copiedId === map.id ? (
|
||||
<span className="copied-text">✓</span>
|
||||
) : (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect
|
||||
x="9"
|
||||
y="9"
|
||||
width="13"
|
||||
height="13"
|
||||
rx="2"
|
||||
ry="2"
|
||||
/>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// User / Auth
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ConnectionInfo {
|
||||
provider: string;
|
||||
provider_username: string | null;
|
||||
@@ -19,10 +15,6 @@ export interface UserInfo {
|
||||
connections: ConnectionInfo[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Maps
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type MapRole = "owner" | "editor" | "viewer";
|
||||
|
||||
/** Map visibility / editability level. */
|
||||
@@ -42,8 +34,8 @@ export interface GridMap {
|
||||
public_access: PublicAccess;
|
||||
owner_id: string; // UUID
|
||||
colors: string[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,10 +84,6 @@ export interface MapAccessRequest {
|
||||
|
||||
export type Tool = "pan" | "zoom" | "draw" | "token";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WebSocket message types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type ClientMessage =
|
||||
| { type: "paint_cell"; x: number; y: number; color: string }
|
||||
| {
|
||||
|
||||
Reference in New Issue
Block a user