style: align all pages with CLAUDE.md design system
- Inputs: uniform padding (8px 12px), borderRadius 7, bg-inset background - List rows/cards: bg-subtle background, border-subtle border (bg-inset was input-only) - Invite/admin badge borders: use accent-border var instead of raw accent - Section headers: 11px, weight 500, uppercase, 0.7px letter-spacing - Notification/status banners: borderRadius 8 - Remove debug console.log statements from SettingsPage avatar upload flow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -254,7 +254,7 @@ export function BandPage() {
|
|||||||
value={folderInput}
|
value={folderInput}
|
||||||
onChange={(e) => setFolderInput(e.target.value)}
|
onChange={(e) => setFolderInput(e.target.value)}
|
||||||
placeholder={`bands/${band.slug}/`}
|
placeholder={`bands/${band.slug}/`}
|
||||||
style={{ width: "100%", padding: "7px 10px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text)", fontSize: 13, fontFamily: "monospace", boxSizing: "border-box" }}
|
style={{ width: "100%", padding: "8px 12px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 7, color: "var(--text)", fontSize: 13, fontFamily: "monospace", boxSizing: "border-box" }}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", gap: 8, marginTop: 8 }}>
|
<div style={{ display: "flex", gap: 8, marginTop: 8 }}>
|
||||||
<button
|
<button
|
||||||
@@ -289,7 +289,7 @@ export function BandPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{inviteLink && (
|
{inviteLink && (
|
||||||
<div style={{ background: "var(--accent-bg)", border: "1px solid var(--accent)", borderRadius: 6, padding: "10px 14px", marginBottom: 12 }}>
|
<div style={{ background: "var(--accent-bg)", border: "1px solid var(--accent-border)", borderRadius: 8, padding: "10px 14px", marginBottom: 12 }}>
|
||||||
<p style={{ color: "var(--text-muted)", fontSize: 11, margin: "0 0 6px" }}>Invite link (copied to clipboard, valid 72h):</p>
|
<p style={{ color: "var(--text-muted)", fontSize: 11, margin: "0 0 6px" }}>Invite link (copied to clipboard, valid 72h):</p>
|
||||||
<code style={{ color: "var(--accent)", fontSize: 12, wordBreak: "break-all" }}>{inviteLink}</code>
|
<code style={{ color: "var(--accent)", fontSize: 12, wordBreak: "break-all" }}>{inviteLink}</code>
|
||||||
<button
|
<button
|
||||||
@@ -305,7 +305,7 @@ export function BandPage() {
|
|||||||
{members?.map((m) => (
|
{members?.map((m) => (
|
||||||
<div
|
<div
|
||||||
key={m.id}
|
key={m.id}
|
||||||
style={{ background: "var(--bg-subtle)", border: "1px solid var(--border)", borderRadius: 6, padding: "10px 14px", display: "flex", justifyContent: "space-between", alignItems: "center" }}
|
style={{ background: "var(--bg-subtle)", border: "1px solid var(--border-subtle)", borderRadius: 8, padding: "10px 14px", display: "flex", justifyContent: "space-between", alignItems: "center" }}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span style={{ fontWeight: 500 }}>{m.display_name}</span>
|
<span style={{ fontWeight: 500 }}>{m.display_name}</span>
|
||||||
@@ -316,7 +316,7 @@ export function BandPage() {
|
|||||||
fontSize: 10, fontFamily: "monospace", padding: "2px 6px", borderRadius: 3,
|
fontSize: 10, fontFamily: "monospace", padding: "2px 6px", borderRadius: 3,
|
||||||
background: m.role === "admin" ? "var(--accent-bg)" : "var(--bg-inset)",
|
background: m.role === "admin" ? "var(--accent-bg)" : "var(--bg-inset)",
|
||||||
color: m.role === "admin" ? "var(--accent)" : "var(--text-muted)",
|
color: m.role === "admin" ? "var(--accent)" : "var(--text-muted)",
|
||||||
border: `1px solid ${m.role === "admin" ? "var(--accent)" : "var(--border)"}`,
|
border: `1px solid ${m.role === "admin" ? "var(--accent-border)" : "var(--border)"}`,
|
||||||
}}>
|
}}>
|
||||||
{m.role}
|
{m.role}
|
||||||
</span>
|
</span>
|
||||||
@@ -355,12 +355,12 @@ export function BandPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{scanning && scanProgress && (
|
{scanning && scanProgress && (
|
||||||
<div style={{ background: "var(--bg-subtle)", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text-muted)", fontSize: 12, padding: "8px 14px", marginBottom: 8, fontFamily: "monospace" }}>
|
<div style={{ background: "var(--bg-subtle)", border: "1px solid var(--border)", borderRadius: 8, color: "var(--text-muted)", fontSize: 12, padding: "8px 14px", marginBottom: 8, fontFamily: "monospace" }}>
|
||||||
{scanProgress}
|
{scanProgress}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{scanMsg && (
|
{scanMsg && (
|
||||||
<div style={{ background: "var(--teal-bg)", border: "1px solid var(--teal)", borderRadius: 6, color: "var(--teal)", fontSize: 12, padding: "8px 14px", marginBottom: 12 }}>
|
<div style={{ background: "var(--teal-bg)", border: "1px solid var(--teal)", borderRadius: 8, color: "var(--teal)", fontSize: 12, padding: "8px 14px", marginBottom: 12 }}>
|
||||||
{scanMsg}
|
{scanMsg}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -373,7 +373,7 @@ export function BandPage() {
|
|||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === "Enter" && title && createMutation.mutate()}
|
onKeyDown={(e) => e.key === "Enter" && title && createMutation.mutate()}
|
||||||
style={{ width: "100%", padding: "8px 12px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text)", marginBottom: 12, fontSize: 14, boxSizing: "border-box" }}
|
style={{ width: "100%", padding: "8px 12px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 7, color: "var(--text)", marginBottom: 12, fontSize: 14, boxSizing: "border-box" }}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", gap: 8 }}>
|
<div style={{ display: "flex", gap: 8 }}>
|
||||||
@@ -425,8 +425,8 @@ export function BandPage() {
|
|||||||
key={s.id}
|
key={s.id}
|
||||||
to={`/bands/${bandId}/sessions/${s.id}`}
|
to={`/bands/${bandId}/sessions/${s.id}`}
|
||||||
style={{
|
style={{
|
||||||
background: "var(--bg-inset)",
|
background: "var(--bg-subtle)",
|
||||||
border: "1px solid var(--border)",
|
border: "1px solid var(--border-subtle)",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: "14px 18px",
|
padding: "14px 18px",
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
@@ -467,8 +467,8 @@ export function BandPage() {
|
|||||||
key={song.id}
|
key={song.id}
|
||||||
to={`/bands/${bandId}/songs/${song.id}`}
|
to={`/bands/${bandId}/songs/${song.id}`}
|
||||||
style={{
|
style={{
|
||||||
background: "var(--bg-inset)",
|
background: "var(--bg-subtle)",
|
||||||
border: "1px solid var(--border)",
|
border: "1px solid var(--border-subtle)",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: "14px 18px",
|
padding: "14px 18px",
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
@@ -512,7 +512,7 @@ export function BandPage() {
|
|||||||
onChange={(e) => setSearchQ(e.target.value)}
|
onChange={(e) => setSearchQ(e.target.value)}
|
||||||
onKeyDown={(e) => { if (e.key === "Enter") { setSearchDirty(true); qc.invalidateQueries({ queryKey: ["songs-search", bandId] }); } }}
|
onKeyDown={(e) => { if (e.key === "Enter") { setSearchDirty(true); qc.invalidateQueries({ queryKey: ["songs-search", bandId] }); } }}
|
||||||
placeholder="Search by name…"
|
placeholder="Search by name…"
|
||||||
style={{ width: "100%", padding: "7px 10px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text)", fontSize: 13, boxSizing: "border-box" }}
|
style={{ width: "100%", padding: "8px 12px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 7, color: "var(--text)", fontSize: 13, boxSizing: "border-box" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -521,7 +521,7 @@ export function BandPage() {
|
|||||||
value={searchKey}
|
value={searchKey}
|
||||||
onChange={(e) => setSearchKey(e.target.value)}
|
onChange={(e) => setSearchKey(e.target.value)}
|
||||||
placeholder="e.g. Am, C, F#"
|
placeholder="e.g. Am, C, F#"
|
||||||
style={{ width: "100%", padding: "7px 10px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text)", fontSize: 13, boxSizing: "border-box" }}
|
style={{ width: "100%", padding: "8px 12px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 7, color: "var(--text)", fontSize: 13, boxSizing: "border-box" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -532,7 +532,7 @@ export function BandPage() {
|
|||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
placeholder="e.g. 80"
|
placeholder="e.g. 80"
|
||||||
style={{ width: "100%", padding: "7px 10px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text)", fontSize: 13, boxSizing: "border-box" }}
|
style={{ width: "100%", padding: "8px 12px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 7, color: "var(--text)", fontSize: 13, boxSizing: "border-box" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -543,7 +543,7 @@ export function BandPage() {
|
|||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
placeholder="e.g. 140"
|
placeholder="e.g. 140"
|
||||||
style={{ width: "100%", padding: "7px 10px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text)", fontSize: 13, boxSizing: "border-box" }}
|
style={{ width: "100%", padding: "8px 12px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 7, color: "var(--text)", fontSize: 13, boxSizing: "border-box" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -571,7 +571,7 @@ export function BandPage() {
|
|||||||
onChange={(e) => setSearchTagInput(e.target.value)}
|
onChange={(e) => setSearchTagInput(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === "Enter" && addTag()}
|
onKeyDown={(e) => e.key === "Enter" && addTag()}
|
||||||
placeholder="Add tag…"
|
placeholder="Add tag…"
|
||||||
style={{ flex: 1, padding: "6px 10px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text)", fontSize: 12 }}
|
style={{ flex: 1, padding: "6px 10px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 7, color: "var(--text)", fontSize: 12 }}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={addTag}
|
onClick={addTag}
|
||||||
@@ -599,8 +599,8 @@ export function BandPage() {
|
|||||||
key={song.id}
|
key={song.id}
|
||||||
to={`/bands/${bandId}/songs/${song.id}`}
|
to={`/bands/${bandId}/songs/${song.id}`}
|
||||||
style={{
|
style={{
|
||||||
background: "var(--bg-inset)",
|
background: "var(--bg-subtle)",
|
||||||
border: "1px solid var(--border)",
|
border: "1px solid var(--border-subtle)",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: "14px 18px",
|
padding: "14px 18px",
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function HomePage() {
|
|||||||
padding: "8px 12px",
|
padding: "8px 12px",
|
||||||
background: "var(--bg-inset)",
|
background: "var(--bg-inset)",
|
||||||
border: "1px solid var(--border)",
|
border: "1px solid var(--border)",
|
||||||
borderRadius: 6,
|
borderRadius: 7,
|
||||||
color: "var(--text)",
|
color: "var(--text)",
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ export function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const inputStyle: React.CSSProperties = {
|
const inputStyle: React.CSSProperties = {
|
||||||
width: "100%", padding: "10px 12px",
|
width: "100%", padding: "8px 12px",
|
||||||
background: "var(--bg-inset)",
|
background: "var(--bg-inset)",
|
||||||
border: "1px solid var(--border)",
|
border: "1px solid var(--border)",
|
||||||
borderRadius: 6,
|
borderRadius: 7,
|
||||||
color: "var(--text)",
|
color: "var(--text)",
|
||||||
marginBottom: 16, fontSize: 14, boxSizing: "border-box",
|
marginBottom: 16, fontSize: 14, boxSizing: "border-box",
|
||||||
};
|
};
|
||||||
@@ -68,7 +68,7 @@ export function LoginPage() {
|
|||||||
style={{ ...inputStyle, marginBottom: 24 }}
|
style={{ ...inputStyle, marginBottom: 24 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button type="submit" style={{ width: "100%", padding: "12px", background: "var(--accent)", border: "none", borderRadius: 6, color: "var(--accent-fg)", fontWeight: 600, cursor: "pointer", fontSize: 14 }}>
|
<button type="submit" style={{ width: "100%", padding: "10px", background: "var(--accent)", border: "none", borderRadius: 7, color: "var(--accent-fg)", fontWeight: 600, cursor: "pointer", fontSize: 14 }}>
|
||||||
{mode === "login" ? "Sign In" : "Create Account"}
|
{mode === "login" ? "Sign In" : "Create Account"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -104,14 +104,14 @@ export function SessionPage() {
|
|||||||
value={labelInput}
|
value={labelInput}
|
||||||
onChange={(e) => setLabelInput(e.target.value)}
|
onChange={(e) => setLabelInput(e.target.value)}
|
||||||
placeholder="e.g. pre-gig warmup"
|
placeholder="e.g. pre-gig warmup"
|
||||||
style={{ width: "100%", padding: "7px 10px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text)", fontSize: 13, boxSizing: "border-box", marginBottom: 10 }}
|
style={{ width: "100%", padding: "8px 12px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 7, color: "var(--text)", fontSize: 13, boxSizing: "border-box", marginBottom: 10 }}
|
||||||
/>
|
/>
|
||||||
<label style={{ display: "block", color: "var(--text-muted)", fontSize: 11, marginBottom: 4 }}>NOTES</label>
|
<label style={{ display: "block", color: "var(--text-muted)", fontSize: 11, marginBottom: 4 }}>NOTES</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={notesInput}
|
value={notesInput}
|
||||||
onChange={(e) => setNotesInput(e.target.value)}
|
onChange={(e) => setNotesInput(e.target.value)}
|
||||||
rows={3}
|
rows={3}
|
||||||
style={{ width: "100%", padding: "7px 10px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text)", fontSize: 13, boxSizing: "border-box", resize: "vertical", fontFamily: "inherit" }}
|
style={{ width: "100%", padding: "8px 12px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 7, color: "var(--text)", fontSize: 13, boxSizing: "border-box", resize: "vertical", fontFamily: "inherit" }}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", gap: 8, marginTop: 10 }}>
|
<div style={{ display: "flex", gap: 8, marginTop: 10 }}>
|
||||||
<button
|
<button
|
||||||
@@ -139,8 +139,8 @@ export function SessionPage() {
|
|||||||
key={song.id}
|
key={song.id}
|
||||||
to={`/bands/${bandId}/songs/${song.id}`}
|
to={`/bands/${bandId}/songs/${song.id}`}
|
||||||
style={{
|
style={{
|
||||||
background: "var(--bg-inset)",
|
background: "var(--bg-subtle)",
|
||||||
border: "1px solid var(--border)",
|
border: "1px solid var(--border-subtle)",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: "14px 18px",
|
padding: "14px 18px",
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const inputStyle: React.CSSProperties = {
|
|||||||
padding: "8px 12px",
|
padding: "8px 12px",
|
||||||
background: "var(--bg-inset)",
|
background: "var(--bg-inset)",
|
||||||
border: "1px solid var(--border)",
|
border: "1px solid var(--border)",
|
||||||
borderRadius: 6,
|
borderRadius: 7,
|
||||||
color: "var(--text)",
|
color: "var(--text)",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
@@ -102,9 +102,6 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
|
|||||||
lastModified: Date.now()
|
lastModified: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Resized image from ${img.width}x${img.height} to ${width}x${height}`);
|
|
||||||
console.log(`File size reduced from ${file.size} to ${resizedFile.size} bytes`);
|
|
||||||
|
|
||||||
resolve(resizedFile);
|
resolve(resizedFile);
|
||||||
}, 'image/jpeg', 0.8); // JPEG quality 80%
|
}, 'image/jpeg', 0.8); // JPEG quality 80%
|
||||||
};
|
};
|
||||||
@@ -144,7 +141,7 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section style={{ marginBottom: 32 }}>
|
<section style={{ marginBottom: 32 }}>
|
||||||
<h2 style={{ fontSize: 13, color: "var(--text-muted)", fontFamily: "monospace", letterSpacing: 1, marginBottom: 16 }}>PROFILE</h2>
|
<h2 style={{ fontSize: 11, fontWeight: 500, color: "var(--text-muted)", textTransform: "uppercase", letterSpacing: "0.7px", marginBottom: 16 }}>PROFILE</h2>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 16, marginBottom: 16 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 16, marginBottom: 16 }}>
|
||||||
{avatarUrl && (
|
{avatarUrl && (
|
||||||
<img src={avatarUrl} alt="Profile" style={{ width: 64, height: 64, borderRadius: "50%", objectFit: "cover" }} />
|
<img src={avatarUrl} alt="Profile" style={{ width: 64, height: 64, borderRadius: "50%", objectFit: "cover" }} />
|
||||||
@@ -158,7 +155,7 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section style={{ marginBottom: 32 }}>
|
<section style={{ marginBottom: 32 }}>
|
||||||
<h2 style={{ fontSize: 13, color: "var(--text-muted)", fontFamily: "monospace", letterSpacing: 1, marginBottom: 8 }}>NEXTCLOUD CONNECTION</h2>
|
<h2 style={{ fontSize: 11, fontWeight: 500, color: "var(--text-muted)", textTransform: "uppercase", letterSpacing: "0.7px", marginBottom: 8 }}>Nextcloud Connection</h2>
|
||||||
<p style={{ color: "var(--text-subtle)", fontSize: 12, marginBottom: 16 }}>
|
<p style={{ color: "var(--text-subtle)", fontSize: 12, marginBottom: 16 }}>
|
||||||
Configure your personal Nextcloud credentials. When set, all file operations (band folders, song uploads, scans) will use these credentials.
|
Configure your personal Nextcloud credentials. When set, all file operations (band folders, song uploads, scans) will use these credentials.
|
||||||
</p>
|
</p>
|
||||||
@@ -190,43 +187,36 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section style={{ marginBottom: 32 }}>
|
<section style={{ marginBottom: 32 }}>
|
||||||
<h2 style={{ fontSize: 13, color: "var(--text-muted)", fontFamily: "monospace", letterSpacing: 1, marginBottom: 16 }}>AVATAR</h2>
|
<h2 style={{ fontSize: 11, fontWeight: 500, color: "var(--text-muted)", textTransform: "uppercase", letterSpacing: "0.7px", marginBottom: 16 }}>AVATAR</h2>
|
||||||
<div style={{ display: "flex", gap: 12, alignItems: "center", marginBottom: 16 }}>
|
<div style={{ display: "flex", gap: 12, alignItems: "center", marginBottom: 16 }}>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
onChange={async (e) => {
|
onChange={async (e) => {
|
||||||
console.log("File input changed", e.target.files);
|
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
console.log("Selected file:", file.name, file.type, file.size);
|
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check file size and resize if needed
|
// Check file size and resize if needed
|
||||||
const maxSize = 4 * 1024 * 1024; // 4MB (more conservative to account for base64 overhead)
|
const maxSize = 4 * 1024 * 1024; // 4MB (more conservative to account for base64 overhead)
|
||||||
let processedFile = file;
|
let processedFile = file;
|
||||||
|
|
||||||
if (file.size > maxSize) {
|
if (file.size > maxSize) {
|
||||||
console.log("File too large, resizing...");
|
|
||||||
processedFile = await resizeImage(file, 800, 800); // Max 800x800
|
processedFile = await resizeImage(file, 800, 800); // Max 800x800
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', processedFile, processedFile.name || file.name);
|
formData.append('file', processedFile, processedFile.name || file.name);
|
||||||
|
|
||||||
console.log("Uploading file to /auth/me/avatar");
|
|
||||||
console.log("Final file size:", processedFile.size);
|
|
||||||
const response = await api.upload<MemberRead>('/auth/me/avatar', formData);
|
const response = await api.upload<MemberRead>('/auth/me/avatar', formData);
|
||||||
console.log("Upload response:", response);
|
|
||||||
|
|
||||||
setAvatarUrl(response.avatar_url || '');
|
setAvatarUrl(response.avatar_url || '');
|
||||||
qc.invalidateQueries({ queryKey: ['me'] });
|
qc.invalidateQueries({ queryKey: ['me'] });
|
||||||
qc.invalidateQueries({ queryKey: ['comments'] });
|
qc.invalidateQueries({ queryKey: ['comments'] });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Upload failed:", err);
|
|
||||||
let errorMessage = 'Failed to upload avatar. Please try again.';
|
let errorMessage = 'Failed to upload avatar. Please try again.';
|
||||||
|
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
errorMessage = err.message;
|
errorMessage = err.message;
|
||||||
if (err.message.includes('413')) {
|
if (err.message.includes('413')) {
|
||||||
@@ -235,8 +225,6 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
|
|||||||
errorMessage = 'Invalid image file. Please upload a valid image (JPG, PNG, etc.).';
|
errorMessage = 'Invalid image file. Please upload a valid image (JPG, PNG, etc.).';
|
||||||
}
|
}
|
||||||
} else if (typeof err === 'object' && err !== null) {
|
} else if (typeof err === 'object' && err !== null) {
|
||||||
// Try to extract more details from the error object
|
|
||||||
console.error("Error details:", JSON.stringify(err));
|
|
||||||
const errorObj = err as { status?: number; data?: { detail?: string } };
|
const errorObj = err as { status?: number; data?: { detail?: string } };
|
||||||
if (errorObj.status === 422 && errorObj.data?.detail) {
|
if (errorObj.status === 422 && errorObj.data?.detail) {
|
||||||
errorMessage = errorObj.data.detail;
|
errorMessage = errorObj.data.detail;
|
||||||
@@ -252,48 +240,39 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
|
|||||||
style={{ display: "none" }}
|
style={{ display: "none" }}
|
||||||
id="avatar-upload"
|
id="avatar-upload"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="avatar-upload" style={{
|
<label htmlFor="avatar-upload" style={{
|
||||||
background: "var(--accent)",
|
background: "var(--accent)",
|
||||||
border: "none",
|
border: "none",
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
color: "var(--accent-fg)",
|
color: "var(--accent-fg)",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
padding: "8px 16px",
|
padding: "8px 16px",
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSize: 14
|
fontSize: 13,
|
||||||
}}>
|
}}>
|
||||||
{uploading ? "Uploading..." : "Upload Avatar"}
|
{uploading ? "Uploading..." : "Upload Avatar"}
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
console.log("Generate Random button clicked");
|
|
||||||
try {
|
try {
|
||||||
// Generate a new random avatar using user ID as seed for consistency
|
|
||||||
const seed = Math.random().toString(36).substring(2, 15);
|
const seed = Math.random().toString(36).substring(2, 15);
|
||||||
const newAvatarUrl = `https://api.dicebear.com/9.x/identicon/svg?seed=${seed}&backgroundType=gradientLinear&size=128`;
|
const newAvatarUrl = `https://api.dicebear.com/9.x/identicon/svg?seed=${seed}&backgroundType=gradientLinear&size=128`;
|
||||||
|
|
||||||
console.log("Generated avatar URL:", newAvatarUrl);
|
|
||||||
console.log("Calling updateSettings with:", { avatar_url: newAvatarUrl });
|
|
||||||
|
|
||||||
await updateSettings({ avatar_url: newAvatarUrl });
|
await updateSettings({ avatar_url: newAvatarUrl });
|
||||||
setAvatarUrl(newAvatarUrl);
|
setAvatarUrl(newAvatarUrl);
|
||||||
qc.invalidateQueries({ queryKey: ["me"] });
|
qc.invalidateQueries({ queryKey: ["me"] });
|
||||||
qc.invalidateQueries({ queryKey: ["comments"] });
|
qc.invalidateQueries({ queryKey: ["comments"] });
|
||||||
|
|
||||||
console.log("Avatar updated successfully");
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to update avatar:", err);
|
|
||||||
setError(err instanceof Error ? err.message : 'Failed to update avatar');
|
setError(err instanceof Error ? err.message : 'Failed to update avatar');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
background: "none",
|
background: "none",
|
||||||
border: "1px solid var(--border)",
|
border: "1px solid var(--border)",
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
color: "var(--text)",
|
color: "var(--text)",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
padding: "8px 16px",
|
padding: "8px 16px",
|
||||||
fontSize: 14
|
fontSize: 13,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Generate Random
|
Generate Random
|
||||||
@@ -306,8 +285,6 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
|
|||||||
alt="Preview"
|
alt="Preview"
|
||||||
style={{ width: 48, height: 48, borderRadius: "50%", objectFit: "cover" }}
|
style={{ width: 48, height: 48, borderRadius: "50%", objectFit: "cover" }}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
console.error("Failed to load avatar:", avatarUrl);
|
|
||||||
// Set to default avatar on error
|
|
||||||
setAvatarUrl(`https://api.dicebear.com/9.x/identicon/svg?seed=${me.id}&backgroundType=gradientLinear&size=128`);
|
setAvatarUrl(`https://api.dicebear.com/9.x/identicon/svg?seed=${me.id}&backgroundType=gradientLinear&size=128`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ export function SongPage() {
|
|||||||
onChange={(e) => setCommentBody(e.target.value)}
|
onChange={(e) => setCommentBody(e.target.value)}
|
||||||
placeholder="Add a comment…"
|
placeholder="Add a comment…"
|
||||||
rows={2}
|
rows={2}
|
||||||
style={{ flex: 1, padding: "8px 12px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text)", fontSize: 13, resize: "vertical", fontFamily: "inherit" }}
|
style={{ flex: 1, padding: "8px 12px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 7, color: "var(--text)", fontSize: 13, resize: "vertical", fontFamily: "inherit" }}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => commentBody.trim() && addCommentMutation.mutate({ body: commentBody.trim(), timestamp: currentTime })}
|
onClick={() => commentBody.trim() && addCommentMutation.mutate({ body: commentBody.trim(), timestamp: currentTime })}
|
||||||
@@ -242,7 +242,7 @@ function AnnotationCard({ annotation: a, onSeek, versionId }: { annotation: Anno
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 8, padding: 14 }}>
|
<div style={{ background: "var(--bg-subtle)", border: "1px solid var(--border-subtle)", borderRadius: 8, padding: 14 }}>
|
||||||
<div style={{ display: "flex", gap: 8, marginBottom: 6 }}>
|
<div style={{ display: "flex", gap: 8, marginBottom: 6 }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => onSeek(a.timestamp_ms / 1000)}
|
onClick={() => onSeek(a.timestamp_ms / 1000)}
|
||||||
|
|||||||
Reference in New Issue
Block a user