refactor: split monolith into 3 microservices (frontend, backend, discord-gateway)
- Extract services into services/{frontend,backend,discord-gateway}
- Create packages/shared/ for shared logger, errors, utils, types
- Setup Modular MVC pattern in backend (controller→service→repository)
- Setup event-driven architecture in discord-gateway with Redis pub/sub
- Move Docker files to infra/docker/ with per-service Dockerfiles
- Update docker-compose.yml to use Traefik-only routing (no port exposes)
- Update GitHub Actions deploy workflow for multi-service matrix build
- Fix all import paths and resolve type errors across all services
- All 3 services pass tsc --noEmit clean
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
bda8304bb9
commit
c48a0c5e3b
@@ -0,0 +1,90 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { MessageRecord } from "../../../shared/api/client";
|
||||
import { ScrollArea } from "../../../shared/ui";
|
||||
import { MessageCard, MessageCardSkeleton } from "./MessageCard";
|
||||
|
||||
export interface MessageFeedProps {
|
||||
messages: MessageRecord[];
|
||||
onReanalyze: (id: string) => Promise<void>;
|
||||
emptyText?: string;
|
||||
loading?: boolean;
|
||||
onLoadMore?: () => void;
|
||||
hasMore?: boolean;
|
||||
loadingMore?: boolean;
|
||||
}
|
||||
|
||||
export function MessageFeed({
|
||||
messages,
|
||||
onReanalyze,
|
||||
emptyText = "No messages found.",
|
||||
loading,
|
||||
onLoadMore,
|
||||
hasMore,
|
||||
loadingMore,
|
||||
}: MessageFeedProps) {
|
||||
// IntersectionObserver for infinite scroll — fires when sentinel becomes visible
|
||||
const sentinelRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!onLoadMore || !hasMore) return;
|
||||
const el = sentinelRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0]?.isIntersecting) onLoadMore();
|
||||
},
|
||||
{ rootMargin: "400px" }, // preload before user reaches bottom
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [onLoadMore, hasMore]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ScrollArea className="h-[calc(100vh-260px)] pr-3">
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<MessageCardSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-dashed border-border p-10 text-center text-sm text-muted-foreground">
|
||||
{emptyText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-[calc(100vh-260px)] pr-3">
|
||||
<div className="space-y-3">
|
||||
{messages.map((message) => (
|
||||
<MessageCard
|
||||
key={message.id}
|
||||
message={message}
|
||||
onReanalyze={onReanalyze}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Infinite-scroll sentinel */}
|
||||
{hasMore && (
|
||||
<div
|
||||
ref={sentinelRef}
|
||||
className="flex items-center justify-center py-4"
|
||||
>
|
||||
{loadingMore ? (
|
||||
<MessageCardSkeleton />
|
||||
) : (
|
||||
<div className="h-2 w-2 rounded-full bg-muted-foreground/40" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user