style(frontend): refactor UI components with sky color palette, improved spacing, and design polish
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { MessageRecord } from "../../../shared/api/client";
|
||||
import { parseMetadata } from "../../../entities/message/types";
|
||||
import { EmptyStateMascot } from "../../../widgets/mascot/ChibiMascot";
|
||||
|
||||
interface ImageItem {
|
||||
url: string;
|
||||
@@ -8,6 +9,17 @@ interface ImageItem {
|
||||
message: MessageRecord;
|
||||
}
|
||||
|
||||
function kindBadge(kind: ImageItem["kind"]): string {
|
||||
switch (kind) {
|
||||
case "sticker":
|
||||
return "bg-pink-100 text-pink-700 border-pink-200";
|
||||
case "attachment":
|
||||
return "bg-primary-soft text-primary border-primary/30";
|
||||
case "embed":
|
||||
return "bg-purple-100 text-purple-700 border-purple-200";
|
||||
}
|
||||
}
|
||||
|
||||
export function ImageGrid({ messages }: { messages: MessageRecord[] }) {
|
||||
const images: ImageItem[] = [];
|
||||
|
||||
@@ -57,9 +69,7 @@ export function ImageGrid({ messages }: { messages: MessageRecord[] }) {
|
||||
|
||||
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>
|
||||
<EmptyStateMascot variant="thinking" message="No images yet~" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,7 +84,7 @@ export function ImageGrid({ messages }: { messages: MessageRecord[] }) {
|
||||
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"
|
||||
className="group overflow-hidden rounded-2xl border border-primary/20 bg-white shadow-sm transition-all hover:border-primary/40 hover:shadow-md"
|
||||
>
|
||||
<div className="relative aspect-video overflow-hidden">
|
||||
{image.kind === "sticker" ? (
|
||||
@@ -92,14 +102,16 @@ export function ImageGrid({ messages }: { messages: MessageRecord[] }) {
|
||||
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">
|
||||
<span
|
||||
className={`absolute right-2 top-2 rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider shadow-sm backdrop-blur ${kindBadge(image.kind)}`}
|
||||
>
|
||||
{image.kind}
|
||||
</div>
|
||||
</span>
|
||||
</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">
|
||||
<div className="h-4 w-4 overflow-hidden rounded-full ring-1 ring-primary/30">
|
||||
<img
|
||||
src={
|
||||
image.message.avatar_url ??
|
||||
|
||||
@@ -94,13 +94,13 @@ function aiVariant(status: string) {
|
||||
function severityColor(severity: string) {
|
||||
switch (severity) {
|
||||
case "critical":
|
||||
return "bg-red-500/20 text-red-300 border-red-500/30";
|
||||
return "bg-red-100 text-red-700 border-red-200";
|
||||
case "high":
|
||||
return "bg-orange-500/20 text-orange-300 border-orange-500/30";
|
||||
return "bg-orange-100 text-orange-700 border-orange-200";
|
||||
case "medium":
|
||||
return "bg-yellow-500/20 text-yellow-300 border-yellow-500/30";
|
||||
return "bg-yellow-100 text-yellow-700 border-yellow-200";
|
||||
case "low":
|
||||
return "bg-blue-500/20 text-blue-300 border-blue-500/30";
|
||||
return "bg-blue-100 text-blue-700 border-blue-200";
|
||||
default:
|
||||
return "bg-muted text-muted-foreground border-border";
|
||||
}
|
||||
@@ -175,7 +175,13 @@ export function MessageCard({
|
||||
|
||||
return (
|
||||
<article
|
||||
className={`group rounded-2xl border border-border bg-card ${compact ? "px-4 py-1.5" : "p-4"} shadow-sm transition-all hover:border-primary/30 hover:shadow-md ${message.deleted_at ? "opacity-60" : ""}`}
|
||||
className={`group rounded-2xl border bg-white shadow-sm transition-all hover:border-primary/30 hover:shadow-md ${
|
||||
compact ? "px-4 py-1.5" : "p-4"
|
||||
} ${
|
||||
message.deleted_at
|
||||
? "border-red-200 opacity-60"
|
||||
: "border-primary/20"
|
||||
}`}
|
||||
>
|
||||
<div className={`flex ${compact ? "gap-2" : "gap-3"}`}>
|
||||
{!compact && (
|
||||
@@ -185,7 +191,7 @@ export function MessageCard({
|
||||
"https://cdn.discordapp.com/embed/avatars/0.png"
|
||||
}
|
||||
alt=""
|
||||
className="h-10 w-10 shrink-0 rounded-full object-cover ring-1 ring-border"
|
||||
className="h-10 w-10 shrink-0 rounded-full object-cover ring-2 ring-primary/30"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
@@ -319,10 +325,10 @@ export function MessageCard({
|
||||
|
||||
{message.ai_analysis ? (
|
||||
<div
|
||||
className={`rounded-xl border-l-2 p-3 ${
|
||||
className={`rounded-xl border-l-4 p-3 ${
|
||||
aiStatus === "flagged"
|
||||
? "border-l-red-500 bg-red-500/5"
|
||||
: "border-l-blue-500 bg-blue-500/5"
|
||||
? "border-l-pink-400 bg-pink-50/50"
|
||||
: "border-l-emerald-400 bg-emerald-50/50"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
@@ -331,9 +337,7 @@ export function MessageCard({
|
||||
className="flex w-full items-center justify-between gap-2 text-left text-xs"
|
||||
>
|
||||
<span className="font-medium text-foreground/80">
|
||||
{aiStatus === "flagged"
|
||||
? "🚨"
|
||||
: "ℹ️"}{" "}
|
||||
{aiStatus === "flagged" ? "🚨 " : "ℹ️ "}
|
||||
{analysisSummary}
|
||||
</span>
|
||||
{showAnalysis ? (
|
||||
@@ -351,7 +355,7 @@ export function MessageCard({
|
||||
) : null}
|
||||
|
||||
{message.ai_error ? (
|
||||
<div className="rounded-xl bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="rounded-xl bg-pink-50/50 p-3 text-sm text-pink-700">
|
||||
AI error: {message.ai_error}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -370,7 +374,7 @@ export function MessageCard({
|
||||
{isReanalyzing ? "Reanalyzing..." : "Re-analyze"}
|
||||
</Button>
|
||||
{aiStatus === "error" && (
|
||||
<span className="text-xs text-destructive/80">
|
||||
<span className="text-xs text-pink-600/80">
|
||||
Click to retry analysis
|
||||
</span>
|
||||
)}
|
||||
@@ -383,7 +387,7 @@ export function MessageCard({
|
||||
|
||||
export function MessageCardSkeleton() {
|
||||
return (
|
||||
<article className="rounded-2xl border border-border bg-card p-4 shadow-sm">
|
||||
<article className="rounded-2xl border border-primary/20 bg-white p-4 shadow-sm">
|
||||
<div className="flex gap-3">
|
||||
<Skeleton className="h-10 w-10 shrink-0 rounded-full" />
|
||||
<div className="min-w-0 flex-1 space-y-3">
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import type { MessageRecord } from "../../../shared/api/client";
|
||||
import {
|
||||
cardStagger,
|
||||
cardItem,
|
||||
} from "../../../shared/hooks/useFramerStagger";
|
||||
import { ScrollArea } from "../../../shared/ui";
|
||||
import { MessageCard, MessageCardSkeleton } from "./MessageCard";
|
||||
import { EmptyStateMascot } from "../../../widgets/mascot/ChibiMascot";
|
||||
|
||||
export interface MessageFeedProps {
|
||||
messages: MessageRecord[];
|
||||
@@ -81,27 +87,29 @@ export function MessageFeed({
|
||||
}
|
||||
|
||||
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 <EmptyStateMascot variant="waving" message="No messages yet~" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-[calc(100vh-260px)] pr-3">
|
||||
<div className="space-y-3">
|
||||
<motion.div
|
||||
className="space-y-3"
|
||||
variants={cardStagger}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
>
|
||||
{groupedMessages.map((group) =>
|
||||
group.messages.map((message, idx) => {
|
||||
const isFirstInGroup = idx === 0;
|
||||
const isCompact = !isFirstInGroup;
|
||||
return (
|
||||
<MessageCard
|
||||
key={message.id}
|
||||
message={message}
|
||||
onReanalyze={onReanalyze}
|
||||
compact={isCompact}
|
||||
/>
|
||||
<motion.div key={message.id} variants={cardItem}>
|
||||
<MessageCard
|
||||
message={message}
|
||||
onReanalyze={onReanalyze}
|
||||
compact={isCompact}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
}),
|
||||
)}
|
||||
@@ -115,11 +123,11 @@ export function MessageFeed({
|
||||
{loadingMore ? (
|
||||
<MessageCardSkeleton />
|
||||
) : (
|
||||
<div className="h-2 w-2 rounded-full bg-muted-foreground/40" />
|
||||
<div className="h-2 w-2 rounded-full bg-primary/40" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,22 @@ interface AlertDetail {
|
||||
brief: string;
|
||||
}
|
||||
|
||||
function severityToToastType(
|
||||
severity: string,
|
||||
): "error" | "warning" | "info" | "success" {
|
||||
switch (severity) {
|
||||
case "critical":
|
||||
case "high":
|
||||
return "error";
|
||||
case "medium":
|
||||
return "warning";
|
||||
case "low":
|
||||
return "info";
|
||||
default:
|
||||
return "warning";
|
||||
}
|
||||
}
|
||||
|
||||
export function ModerationAlertListener() {
|
||||
const { addToast } = useToast();
|
||||
|
||||
@@ -29,7 +45,7 @@ export function ModerationAlertListener() {
|
||||
|
||||
addToast(
|
||||
`🚨 ${username} ${sevLabel}${catLabel}: ${brief}`,
|
||||
"error",
|
||||
severityToToastType(severity),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user