refactor(frontend): major rebuild — feature-sliced architecture + bug fixes + glass-morphism UI
Architecture: - Feature-sliced directory: entities/, shared/, features/, widgets/ - Consolidated API client (shared/api/client.ts) — all endpoints in one file - Single WebSocket manager (shared/ws/socket.ts) with typed event bus - Extracted hooks: useAudioPlayback, useAudioTransmit, useLocalStorage, useUIState - UI primitives moved to shared/ui/ with barrel export - Added Skeleton, Toast, MobileTabBar components Bug fixes (8/8): 1. useMemo→useEffect in RecordingsSubPanel (async side-effect anti-pattern) 2. ArrayBuffer.slice() before WebSocket send (shared buffer bug) 3. Proper useEffect dependency arrays throughout 4. Stable React keys (no index fallbacks) 5. onReanalyze properly awaited (Promise<void> return) 6. monitorGuild memoized with useMemo 7. localStorage validation with shape checking 8. Deleted duplicate socket logic (ws/client.ts removed) UI polish: - Glass-morphism design tokens (backdrop-blur, translucent cards) - Gradient mesh background with subtle radial overlays - Expandable sidebar + mobile bottom tab bar - Skeleton loading placeholders - Audio visualizer with CSS pulse animation Deleted: src/api/, src/components/, src/hooks/, src/types/, src/ws/, src/lib/ Added: 40 new files across feature-sliced structure Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1aab0d1df1
commit
f5507e01f6
@@ -0,0 +1,104 @@
|
||||
import type { MessageRecord } from "../../../shared/api/client";
|
||||
|
||||
interface MessageMetadata {
|
||||
stickers?: Array<{ name?: string; url?: string }>;
|
||||
attachments?: Array<{ name: string; url: string; contentType?: string }>;
|
||||
embeds?: Array<{ title?: string; image?: string; thumbnail?: string }>;
|
||||
}
|
||||
|
||||
interface ImageItem {
|
||||
url: string;
|
||||
title: string;
|
||||
kind: "attachment" | "embed" | "sticker";
|
||||
message: MessageRecord;
|
||||
}
|
||||
|
||||
function parseMetadata(value: string | null): MessageMetadata {
|
||||
if (!value) return {};
|
||||
try { return JSON.parse(value) as MessageMetadata; } catch { return {}; }
|
||||
}
|
||||
|
||||
export function ImageGrid({ messages }: { messages: MessageRecord[] }) {
|
||||
const images: ImageItem[] = [];
|
||||
|
||||
for (const message of messages) {
|
||||
const metadata = parseMetadata(message.metadata);
|
||||
|
||||
// Stickers
|
||||
for (const sticker of metadata.stickers ?? []) {
|
||||
if (sticker.url) {
|
||||
images.push({ url: sticker.url, title: sticker.name || "sticker", kind: "sticker", message });
|
||||
}
|
||||
}
|
||||
|
||||
// Attachments
|
||||
for (const attachment of metadata.attachments ?? []) {
|
||||
if (attachment.url && (attachment.contentType?.startsWith("image/") || /\.(png|jpe?g|gif|webp)$/i.test(attachment.name))) {
|
||||
images.push({ url: attachment.url, title: attachment.name, kind: "attachment", message });
|
||||
}
|
||||
}
|
||||
|
||||
// Embed images
|
||||
for (const embed of metadata.embeds ?? []) {
|
||||
for (const imgUrl of [embed.image, embed.thumbnail].filter(Boolean)) {
|
||||
images.push({ url: imgUrl as string, title: embed.title || "embed image", kind: "embed", message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (images.length === 0) {
|
||||
return <div className="rounded-2xl border border-dashed border-border p-10 text-center text-sm text-muted-foreground">No images found.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
{images.map((image, index) => {
|
||||
// Stable key using message.id + url
|
||||
const stableKey = `${image.message.id}-${image.kind}-${index}`;
|
||||
return (
|
||||
<a
|
||||
key={stableKey}
|
||||
href={image.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="group overflow-hidden rounded-2xl border border-border bg-card shadow-sm transition-all hover:border-primary/30 hover:shadow-md"
|
||||
>
|
||||
<div className="relative aspect-video overflow-hidden">
|
||||
{image.kind === "sticker" ? (
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.title}
|
||||
className="h-full w-full object-contain bg-muted/30 p-2 transition-transform group-hover:scale-105"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.title}
|
||||
className="h-full w-full object-cover transition-transform group-hover:scale-105"
|
||||
loading="lazy"
|
||||
/>
|
||||
)}
|
||||
<div className="absolute right-2 top-2 rounded-md bg-black/60 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wider text-white backdrop-blur">
|
||||
{image.kind}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<div className="truncate text-sm font-medium">{image.title}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 overflow-hidden rounded-full">
|
||||
<img
|
||||
src={image.message.avatar_url ?? "https://cdn.discordapp.com/embed/avatars/0.png"}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<span className="truncate text-xs text-muted-foreground">{image.message.username}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user