Add profile changes for name, email, password, avatar
This commit is contained in:
1 parent
27c6d86d50
commit
063b9c9510
13 files changed
+328
-17
No files matched your search
@@ -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 & badges">
|
||||
🖼️ {user.username}
|
||||
<Link href="/profile" className={cls("/profile")} aria-current={current("/profile")} title="My profile, drawings & 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 />
|
||||
</>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in new issue
Block a user