Add profile changes for name, email, password, avatar

This commit is contained in:
iamdoubz committed 2026-06-20 12:46:06 -05:00
1 parent 27c6d86d50
commit 063b9c9510
13 files changed
+328 -17

No files matched your search

+8 -3
View File
@@ -6,7 +6,7 @@ import LogoutButton from "./LogoutButton";
export default function NavLinks({
user,
}: {
user: { username: string; isAdmin: boolean } | null;
user: { username: string; isAdmin: boolean; displayName?: string; avatar?: string } | null;
}) {
const pathname = usePathname() || "/";
@@ -34,8 +34,13 @@ export default function NavLinks({
<Link href="/report" className={cls("/report")} aria-current={current("/report")}>
Report a problem
</Link>
<Link href="/profile" className={cls("/profile")} aria-current={current("/profile")} title="My drawings &amp; badges">
🖼️ {user.username}
<Link href="/profile" className={cls("/profile")} aria-current={current("/profile")} title="My profile, drawings &amp; badges" style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
{user.avatar ? (
<img src={user.avatar} alt="" style={{ width: 24, height: 24, borderRadius: "50%", objectFit: "cover" }} />
) : (
"🖼️"
)}{" "}
{user.displayName || user.username}
</Link>
<LogoutButton />
</>
+3 -4
View File
@@ -10,6 +10,7 @@ import {
saveFinalDrawing,
exportCanvasToDataURL,
fileToDataURL,
loadDrawingAsLayer,
} from "./drawingPersistence";
interface StepData { n: number; title: string; instruction: string; tip: string; guideSvg: string; }
@@ -49,7 +50,7 @@ export default function ShadingRunner({
const stageRef = useRef<HTMLDivElement | null>(null);
const strokesRef = useRef<Stroke[]>([]);
const drawingRef = useRef<Stroke | null>(null);
const baseImgRef = useRef<HTMLImageElement | null>(null);
const baseImgRef = useRef<HTMLCanvasElement | null>(null);
const saveTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const [toneIdx, setToneIdx] = useState(0);
const [size, setSize] = useState(SIZES[1]);
@@ -111,9 +112,7 @@ export default function ShadingRunner({
let alive = true;
loadProgressDrawing(meta.level, meta.sublevel).then((img) => {
if (!img || !alive) return;
const im = new Image();
im.onload = () => { baseImgRef.current = im; redraw(); };
im.src = img;
loadDrawingAsLayer(img).then((cv) => { if (alive) { baseImgRef.current = cv; redraw(); } });
});
return () => { alive = false; };
}, [loggedIn, alreadyEarned, meta.level, meta.sublevel, redraw]);
+1 -1
View File
@@ -10,7 +10,7 @@ export default async function SiteNav() {
<Link href="/" className="brand">
<span className="logo">🐟</span> DrawIt
</Link>
<NavLinks user={user ? { username: user.username, isAdmin: user.role === "admin" } : null} />
<NavLinks user={user ? { username: user.username, isAdmin: user.role === "admin", displayName: user.display_name ?? "", avatar: user.avatar ?? "" } : null} />
</div>
</nav>
);
+3 -3
View File
@@ -4,7 +4,7 @@ import Link from "next/link";
import { useRouter } from "next/navigation";
import { badgeSvgMarkup, badgeFullSvg } from "./BadgeArt";
import LineDraw from "./LineDraw";
import { loadProgressDrawing, saveProgressDrawing, saveFinalDrawing, exportCanvasToDataURL, fileToDataURL } from "./drawingPersistence";
import { loadProgressDrawing, saveProgressDrawing, saveFinalDrawing, exportCanvasToDataURL, fileToDataURL, loadDrawingAsLayer } from "./drawingPersistence";
interface StepData { n: number; title: string; instruction: string; tip: string; lines: string[] }
interface Meta {
@@ -41,7 +41,7 @@ export default function TraceRunner({ meta, steps, loggedIn, username, completed
const stageRef = useRef<HTMLDivElement | null>(null);
const strokesRef = useRef<Stroke[]>([]);
const drawingRef = useRef<Stroke | null>(null);
const baseImgRef = useRef<HTMLImageElement | null>(null);
const baseImgRef = useRef<HTMLCanvasElement | null>(null);
const saveTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const [color, setColor] = useState(COLORS[0]);
const [size, setSize] = useState(SIZES[1]);
@@ -77,7 +77,7 @@ export default function TraceRunner({ meta, steps, loggedIn, username, completed
// Use this lesson's own saved canvas if any; otherwise carry over the previous lesson's drawing
const src = img || carryImage;
if (!src || !alive) return;
const im = new Image(); im.onload = () => { baseImgRef.current = im; redraw(); }; im.src = src;
loadDrawingAsLayer(src).then((cv) => { if (alive) { baseImgRef.current = cv; redraw(); } });
});
return () => { alive = false; };
}, [loggedIn, alreadyEarned, meta.level, meta.sublevel, carryImage, redraw]);
+31
View File
@@ -60,6 +60,37 @@ export function exportCanvasToDataURL(canvas: HTMLCanvasElement, maxW = 800, whi
return out.toDataURL("image/png");
}
/**
* Load a saved drawing as a transparent layer: the white background is knocked out so only the
* strokes remain. This lets a restored/imported drawing sit over the guide + line video without
* hiding them — works whether the image was saved on white or already transparent.
*/
export function loadDrawingAsLayer(dataUrl: string): Promise<HTMLCanvasElement> {
return new Promise((resolve, reject) => {
const im = new Image();
im.onerror = reject;
im.onload = () => {
const c = document.createElement("canvas");
c.width = im.naturalWidth || 800;
c.height = im.naturalHeight || 600;
const ctx = c.getContext("2d")!;
ctx.drawImage(im, 0, 0);
try {
const id = ctx.getImageData(0, 0, c.width, c.height);
const d = id.data;
for (let i = 0; i < d.length; i += 4) {
if (d[i] > 244 && d[i + 1] > 244 && d[i + 2] > 244) d[i + 3] = 0;
}
ctx.putImageData(id, 0, 0);
} catch {
/* cross-origin shouldn't happen for our own data URLs */
}
resolve(c);
};
im.src = dataUrl;
});
}
/** Read an uploaded image File into a downscaled JPEG data URL (keeps photos small). */
export function fileToDataURL(file: File, maxW = 1000): Promise<string> {
return new Promise((resolve, reject) => {