diff --git a/packages/backend/src/db/queries/users.ts b/packages/backend/src/db/queries/users.ts index 5812332..7cc0e80 100644 --- a/packages/backend/src/db/queries/users.ts +++ b/packages/backend/src/db/queries/users.ts @@ -22,7 +22,7 @@ export async function updateById( updates: { name?: string; passwordHash?: string; - avatarURL?: string; + avatarURL?: string | null; }, ): Promise { const [user] = await db.update(User).set(updates).where(eq(User.id, id)).returning(); diff --git a/packages/backend/src/routes/user/update.ts b/packages/backend/src/routes/user/update.ts index 3111072..94127ca 100644 --- a/packages/backend/src/routes/user/update.ts +++ b/packages/backend/src/routes/user/update.ts @@ -18,7 +18,8 @@ export default async function update(req: AuthedRequest) { const name = url.searchParams.get("name") || undefined; const password = url.searchParams.get("password") || undefined; - const avatarURL = url.searchParams.get("avatarURL") || undefined; + const avatarURL = + url.searchParams.get("avatarURL") === "null" ? null : url.searchParams.get("avatarURL") || undefined; let passwordHash: string | undefined; if (password !== undefined) { passwordHash = await hashPassword(password); diff --git a/packages/frontend/src/Account.tsx b/packages/frontend/src/Account.tsx index b8bd1dd..9d8f1b0 100644 --- a/packages/frontend/src/Account.tsx +++ b/packages/frontend/src/Account.tsx @@ -9,6 +9,7 @@ import { user } from "@/lib/server"; function Account() { const [name, setName] = useState(""); + const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [avatarURL, setAvatarUrl] = useState(null); const [error, setError] = useState(""); @@ -20,6 +21,7 @@ function Account() { if (userStr) { const user = JSON.parse(userStr) as UserRecord; setName(user.name); + setUsername(user.username); setUserId(user.id); setAvatarUrl(user.avatarURL || null); } @@ -42,11 +44,12 @@ function Account() { id: userId, name: name.trim(), password: password.trim(), - avatarURL: avatarURL || undefined, + avatarURL: avatarURL, onSuccess: (data) => { setError(""); localStorage.setItem("user", JSON.stringify(data)); setPassword(""); + window.location.reload(); }, onError: (errorMessage) => { setError(errorMessage); @@ -58,7 +61,24 @@ function Account() {

Account Details

- + + {avatarURL && ( + + )} - ); +const FALLBACK_COLOURS = [ + "bg-red-500", + "bg-orange-500", + "bg-amber-500", + "bg-yellow-500", + "bg-lime-500", + "bg-green-500", + "bg-emerald-500", + "bg-teal-500", + "bg-cyan-500", + "bg-sky-500", + "bg-blue-500", + "bg-indigo-500", + "bg-violet-500", + "bg-purple-500", + "bg-fuchsia-500", + "bg-pink-500", + "bg-rose-500", +]; + +function hashStringToIndex(value: string, modulo: number) { + let hash = 0; + for (let i = 0; i < value.length; i++) { + hash = (hash * 31 + value.charCodeAt(i)) >>> 0; } + return modulo === 0 ? 0 : hash % modulo; +} + +function getInitials(username: string) { + username = username.trim(); + + const parts = username.split(/[^a-zA-Z0-9]+/).filter(Boolean); + if (parts.length === 0) return username.slice(0, 2).toUpperCase(); + + if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); + + return `${parts[0][0]}${parts[1][0]}`.toUpperCase(); +} + +export default function Avatar({ + avatarURL, + name, + username, + size, + textClass = "text-xs", +}: { + avatarURL?: string | null; + name?: string; + username?: string; + size?: number; + textClass?: string; +}) { + const backgroundClass = username + ? FALLBACK_COLOURS[hashStringToIndex(username, FALLBACK_COLOURS.length)] + : "bg-muted"; return (
- {user && } + {avatarURL ? ( + Avatar + ) : name ? ( + {getInitials(name)} + ) : ( + + )}
); } diff --git a/packages/frontend/src/components/issues-table.tsx b/packages/frontend/src/components/issues-table.tsx index 990859b..6fda854 100644 --- a/packages/frontend/src/components/issues-table.tsx +++ b/packages/frontend/src/components/issues-table.tsx @@ -50,8 +50,15 @@ export function IssuesTable({ {issueData.Issue.description} )} {(columns.assignee == null || columns.assignee === true) && ( - - + + {issueData.User && ( + + )} )} diff --git a/packages/frontend/src/components/login-form.tsx b/packages/frontend/src/components/login-form.tsx index 05955f2..e6a432f 100644 --- a/packages/frontend/src/components/login-form.tsx +++ b/packages/frontend/src/components/login-form.tsx @@ -129,6 +129,8 @@ export default function LogInForm() { {mode === "register" && ( <> - + {user.name} ); diff --git a/packages/frontend/src/components/upload-avatar.tsx b/packages/frontend/src/components/upload-avatar.tsx index b30ad55..341a1d0 100644 --- a/packages/frontend/src/components/upload-avatar.tsx +++ b/packages/frontend/src/components/upload-avatar.tsx @@ -4,13 +4,17 @@ import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { user } from "@/lib/server"; import { cn } from "@/lib/utils"; +import Avatar from "./avatar"; export function UploadAvatar({ + name, + username, avatarURL, onAvatarUploaded, - label, className, }: { + name?: string; + username?: string; avatarURL?: string | null; onAvatarUploaded: (avatarURL: string) => void; label?: string; @@ -43,23 +47,28 @@ export function UploadAvatar({ return (
- {avatarURL && ( - - )} + - - {!avatarURL && ( - - )} {error && }
); diff --git a/packages/frontend/src/lib/server/user/update.ts b/packages/frontend/src/lib/server/user/update.ts index 42d0c67..55fde0b 100644 --- a/packages/frontend/src/lib/server/user/update.ts +++ b/packages/frontend/src/lib/server/user/update.ts @@ -12,15 +12,13 @@ export async function update({ id: number; name: string; password: string; - avatarURL?: string; + avatarURL: string | null; } & ServerQueryInput) { const url = new URL(`${getServerURL()}/user/update`); url.searchParams.set("id", `${id}`); url.searchParams.set("name", name.trim()); url.searchParams.set("password", password.trim()); - if (avatarURL) { - url.searchParams.set("avatarURL", avatarURL); - } + url.searchParams.set("avatarURL", avatarURL || "null"); const res = await fetch(url.toString(), { headers: getAuthHeaders(), diff --git a/todo.md b/todo.md index cef05d5..552a6bc 100644 --- a/todo.md +++ b/todo.md @@ -8,5 +8,3 @@ - status - sprints - time tracking (linked to issues or standalone) -- for users without avatars, a random colour + initials is shown - - use username as a seed in a random selection of colour list