mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-29 19:35:20 +02:00
feat(epic-4): implement Content Creation & Productivity components
- Add ChartCapturePanel.tsx for chart screenshot with annotations * One-click chart capture from DexScreener * Auto-add metadata overlay (token info, price, volume, liquidity, timestamp) * Template styles (dark, light, neon) * Export options (Twitter 1200x675, Telegram square, Instagram 1080x1080, clipboard) * Watermark option - Add AnnotationTools.tsx for drawing tools * Line tool for trend lines, support/resistance * Arrow tool for directional indicators * Text tool for labels * Shape tools (circle, rectangle) * Fibonacci retracement tool * Color picker with 6 preset colors * Undo/Redo functionality - Add ThreadGeneratorPanel.tsx for AI-powered Twitter thread generation * Auto-fill token info from current page * Customizable thread length (5-10 tweets) * Tone selection (bullish/neutral/bearish) * AI-generated thread structure (Hook → Analysis → Implications → Conclusion) * Edit individual tweets inline * Add/delete tweets dynamically * Reorder tweets support * Export options (copy all, tweet directly via Twitter API) * Mock thread generation with realistic crypto content - Add ProductivitySettings.tsx for productivity settings management * Notification settings with priority levels (high/medium/low) * Quiet hours configuration (start/end time) * Group notifications and smart batching (5+ alerts) * Keyboard shortcuts display and customization * Quick actions settings (context menu, auto-detect addresses) * Pe * Pe * Pe * Pe * Pttings support Implements Stories 4.1, 4.2, 4.3 from Epic 4: Content Creation & Productivity
This commit is contained in:
parent
ea2080619b
commit
9f75abf0a5
4 changed files with 1198 additions and 0 deletions
|
|
@ -0,0 +1,171 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import {
|
||||||
|
Minus,
|
||||||
|
ArrowRight,
|
||||||
|
Type,
|
||||||
|
Circle,
|
||||||
|
Square,
|
||||||
|
TrendingUp,
|
||||||
|
Eraser,
|
||||||
|
Undo,
|
||||||
|
Redo,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Button } from "@/routes/ui/button";
|
||||||
|
|
||||||
|
export type AnnotationType = "line" | "arrow" | "text" | "circle" | "rectangle" | "fibonacci";
|
||||||
|
|
||||||
|
export interface Annotation {
|
||||||
|
id: string;
|
||||||
|
type: AnnotationType;
|
||||||
|
coordinates: { x: number; y: number }[];
|
||||||
|
text?: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnnotationToolsProps {
|
||||||
|
/** Callback when annotation is added */
|
||||||
|
onAnnotationAdd?: (annotation: Annotation) => void;
|
||||||
|
/** Callback when annotation is removed */
|
||||||
|
onAnnotationRemove?: (id: string) => void;
|
||||||
|
/** Callback when undo is clicked */
|
||||||
|
onUndo?: () => void;
|
||||||
|
/** Callback when redo is clicked */
|
||||||
|
onRedo?: () => void;
|
||||||
|
/** Additional class names */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AnnotationTools - Drawing tools for chart annotations
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Line tool for trend lines, support/resistance
|
||||||
|
* - Arrow tool for directional indicators
|
||||||
|
* - Text tool for labels
|
||||||
|
* - Shape tools (circle, rectangle)
|
||||||
|
* - Fibonacci retracement tool
|
||||||
|
* - Color picker
|
||||||
|
* - Undo/Redo functionality
|
||||||
|
*/
|
||||||
|
export function AnnotationTools({
|
||||||
|
onAnnotationAdd,
|
||||||
|
onAnnotationRemove,
|
||||||
|
onUndo,
|
||||||
|
onRedo,
|
||||||
|
className,
|
||||||
|
}: AnnotationToolsProps) {
|
||||||
|
const [selectedTool, setSelectedTool] = useState<AnnotationType | null>(null);
|
||||||
|
const [selectedColor, setSelectedColor] = useState("#3b82f6"); // blue-500
|
||||||
|
|
||||||
|
const tools: { type: AnnotationType; icon: any; label: string }[] = [
|
||||||
|
{ type: "line", icon: Minus, label: "Line" },
|
||||||
|
{ type: "arrow", icon: ArrowRight, label: "Arrow" },
|
||||||
|
{ type: "text", icon: Type, label: "Text" },
|
||||||
|
{ type: "circle", icon: Circle, label: "Circle" },
|
||||||
|
{ type: "rectangle", icon: Square, label: "Rectangle" },
|
||||||
|
{ type: "fibonacci", icon: TrendingUp, label: "Fibonacci" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
{ value: "#3b82f6", label: "Blue" },
|
||||||
|
{ value: "#ef4444", label: "Red" },
|
||||||
|
{ value: "#22c55e", label: "Green" },
|
||||||
|
{ value: "#eab308", label: "Yellow" },
|
||||||
|
{ value: "#a855f7", label: "Purple" },
|
||||||
|
{ value: "#ffffff", label: "White" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleToolSelect = (tool: AnnotationType) => {
|
||||||
|
setSelectedTool(tool === selectedTool ? null : tool);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("space-y-3", className)}>
|
||||||
|
{/* Drawing Tools */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-xs font-semibold text-muted-foreground">Drawing Tools</h4>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{tools.map(({ type, icon: Icon, label }) => (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-center gap-1 p-2 rounded border transition-colors",
|
||||||
|
selectedTool === type
|
||||||
|
? "bg-primary text-primary-foreground border-primary"
|
||||||
|
: "bg-muted hover:bg-muted/80"
|
||||||
|
)}
|
||||||
|
onClick={() => handleToolSelect(type)}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
<span className="text-xs">{label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Color Picker */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-xs font-semibold text-muted-foreground">Color</h4>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{colors.map(({ value, label }) => (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
className={cn(
|
||||||
|
"w-8 h-8 rounded-full border-2 transition-all",
|
||||||
|
selectedColor === value
|
||||||
|
? "border-primary scale-110"
|
||||||
|
: "border-muted hover:scale-105"
|
||||||
|
)}
|
||||||
|
style={{ backgroundColor: value }}
|
||||||
|
onClick={() => setSelectedColor(value)}
|
||||||
|
title={label}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={onUndo}
|
||||||
|
>
|
||||||
|
<Undo className="h-3 w-3 mr-1" />
|
||||||
|
Undo
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={onRedo}
|
||||||
|
>
|
||||||
|
<Redo className="h-3 w-3 mr-1" />
|
||||||
|
Redo
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSelectedTool(null)}
|
||||||
|
>
|
||||||
|
<Eraser className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Instructions */}
|
||||||
|
{selectedTool && (
|
||||||
|
<div className="p-2 bg-muted/50 rounded text-xs text-muted-foreground">
|
||||||
|
{selectedTool === "line" && "Click and drag to draw a line"}
|
||||||
|
{selectedTool === "arrow" && "Click and drag to draw an arrow"}
|
||||||
|
{selectedTool === "text" && "Click to add text label"}
|
||||||
|
{selectedTool === "circle" && "Click and drag to draw a circle"}
|
||||||
|
{selectedTool === "rectangle" && "Click and drag to draw a rectangle"}
|
||||||
|
{selectedTool === "fibonacci" && "Click two points to draw Fibonacci retracement"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,271 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import {
|
||||||
|
Camera,
|
||||||
|
Download,
|
||||||
|
Copy,
|
||||||
|
Twitter,
|
||||||
|
MessageSquare,
|
||||||
|
Image as ImageIcon,
|
||||||
|
Palette,
|
||||||
|
Settings,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Button } from "@/routes/ui/button";
|
||||||
|
import { AnnotationTools } from "./AnnotationTools";
|
||||||
|
|
||||||
|
export interface ChartCaptureMetadata {
|
||||||
|
tokenSymbol: string;
|
||||||
|
tokenName: string;
|
||||||
|
price: number;
|
||||||
|
change24h: number;
|
||||||
|
volume: number;
|
||||||
|
liquidity: number;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChartCaptureSettings {
|
||||||
|
style: "dark" | "light" | "neon";
|
||||||
|
includeTokenInfo: boolean;
|
||||||
|
includePriceChange: boolean;
|
||||||
|
includeVolumeLiquidity: boolean;
|
||||||
|
includeTimestamp: boolean;
|
||||||
|
includeWatermark: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChartCapturePanelProps {
|
||||||
|
/** Current token metadata */
|
||||||
|
metadata?: ChartCaptureMetadata;
|
||||||
|
/** Callback when capture is clicked */
|
||||||
|
onCapture?: () => void;
|
||||||
|
/** Callback when export is clicked */
|
||||||
|
onExport?: (format: "twitter" | "telegram" | "instagram" | "clipboard") => void;
|
||||||
|
/** Additional class names */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChartCapturePanel - Chart screenshot tool with annotations
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - One-click chart capture from DexScreener
|
||||||
|
* - Auto-add metadata overlay (token info, price, volume, etc.)
|
||||||
|
* - Drawing tools (lines, arrows, text, shapes, Fibonacci)
|
||||||
|
* - Template styles (dark, light, neon)
|
||||||
|
* - Export options (Twitter, Telegram, Instagram, clipboard)
|
||||||
|
*/
|
||||||
|
export function ChartCapturePanel({
|
||||||
|
metadata,
|
||||||
|
onCapture,
|
||||||
|
onExport,
|
||||||
|
className,
|
||||||
|
}: ChartCapturePanelProps) {
|
||||||
|
const [settings, setSettings] = useState<ChartCaptureSettings>({
|
||||||
|
style: "dark",
|
||||||
|
includeTokenInfo: true,
|
||||||
|
includePriceChange: true,
|
||||||
|
includeVolumeLiquidity: true,
|
||||||
|
includeTimestamp: true,
|
||||||
|
includeWatermark: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [capturedImage, setCapturedImage] = useState<string | null>(null);
|
||||||
|
const [isCapturing, setIsCapturing] = useState(false);
|
||||||
|
|
||||||
|
const handleCapture = async () => {
|
||||||
|
setIsCapturing(true);
|
||||||
|
await onCapture?.();
|
||||||
|
// Mock: simulate capture
|
||||||
|
setTimeout(() => {
|
||||||
|
setCapturedImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==");
|
||||||
|
setIsCapturing(false);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExport = (format: "twitter" | "telegram" | "instagram" | "clipboard") => {
|
||||||
|
onExport?.(format);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
if (value >= 1000000) return `$${(value / 1000000).toFixed(2)}M`;
|
||||||
|
if (value >= 1000) return `$${(value / 1000).toFixed(1)}K`;
|
||||||
|
return `$${value.toFixed(2)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-col h-full", className)}>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Camera className="h-5 w-5 text-primary" />
|
||||||
|
<div>
|
||||||
|
<h2 className="font-semibold">Chart Capture</h2>
|
||||||
|
{metadata && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{metadata.tokenSymbol}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||||
|
{/* Capture Button */}
|
||||||
|
{!capturedImage && (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleCapture}
|
||||||
|
disabled={isCapturing}
|
||||||
|
>
|
||||||
|
<Camera className="h-4 w-4 mr-2" />
|
||||||
|
{isCapturing ? "Capturing..." : "Capture Chart"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Preview */}
|
||||||
|
{capturedImage && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="relative border rounded-lg overflow-hidden bg-muted/50">
|
||||||
|
<img
|
||||||
|
src={capturedImage}
|
||||||
|
alt="Captured chart"
|
||||||
|
className="w-full h-auto"
|
||||||
|
/>
|
||||||
|
{/* Metadata Overlay Preview */}
|
||||||
|
{metadata && settings.includeTokenInfo && (
|
||||||
|
<div className="absolute top-2 left-2 bg-background/90 backdrop-blur-sm p-2 rounded text-xs">
|
||||||
|
<div className="font-bold">{metadata.tokenSymbol}</div>
|
||||||
|
{settings.includePriceChange && (
|
||||||
|
<div className={cn(
|
||||||
|
"font-semibold",
|
||||||
|
metadata.change24h >= 0 ? "text-green-600" : "text-red-600"
|
||||||
|
)}>
|
||||||
|
${metadata.price.toFixed(6)} ({metadata.change24h >= 0 ? "+" : ""}{metadata.change24h.toFixed(2)}%)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Annotation Tools */}
|
||||||
|
<AnnotationTools />
|
||||||
|
|
||||||
|
{/* Recapture Button */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => setCapturedImage(null)}
|
||||||
|
>
|
||||||
|
<Camera className="h-4 w-4 mr-2" />
|
||||||
|
Recapture
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Style Selection */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Palette className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<h3 className="font-semibold text-sm">Style</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{(["dark", "light", "neon"] as const).map((style) => (
|
||||||
|
<button
|
||||||
|
key={style}
|
||||||
|
className={cn(
|
||||||
|
"flex-1 p-2 rounded border text-xs font-medium transition-colors",
|
||||||
|
settings.style === style
|
||||||
|
? "bg-primary text-primary-foreground border-primary"
|
||||||
|
: "bg-muted hover:bg-muted/80"
|
||||||
|
)}
|
||||||
|
onClick={() => setSettings({ ...settings, style })}
|
||||||
|
>
|
||||||
|
{style.charAt(0).toUpperCase() + style.slice(1)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Metadata Options */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Settings className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<h3 className="font-semibold text-sm">Metadata</h3>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{[
|
||||||
|
{ key: "includeTokenInfo" as const, label: "Token info" },
|
||||||
|
{ key: "includePriceChange" as const, label: "Price & change" },
|
||||||
|
{ key: "includeVolumeLiquidity" as const, label: "Volume & liquidity" },
|
||||||
|
{ key: "includeTimestamp" as const, label: "Timestamp" },
|
||||||
|
{ key: "includeWatermark" as const, label: "Watermark" },
|
||||||
|
].map(({ key, label }) => (
|
||||||
|
<label key={key} className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings[key]}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings({ ...settings, [key]: e.target.checked })
|
||||||
|
}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<span className="text-sm">{label}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer - Export Options */}
|
||||||
|
{capturedImage && (
|
||||||
|
<div className="border-t p-3 space-y-2">
|
||||||
|
<h3 className="font-semibold text-xs text-muted-foreground mb-2">Export</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleExport("twitter")}
|
||||||
|
>
|
||||||
|
<Twitter className="h-3 w-3 mr-1" />
|
||||||
|
Twitter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleExport("telegram")}
|
||||||
|
>
|
||||||
|
<MessageSquare className="h-3 w-3 mr-1" />
|
||||||
|
Telegram
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleExport("instagram")}
|
||||||
|
>
|
||||||
|
<ImageIcon className="h-3 w-3 mr-1" />
|
||||||
|
Instagram
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleExport("clipboard")}
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3 mr-1" />
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => handleExport("clipboard")}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Save to File
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,382 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import {
|
||||||
|
MessageSquare,
|
||||||
|
Sparkles,
|
||||||
|
Copy,
|
||||||
|
Twitter,
|
||||||
|
Edit2,
|
||||||
|
Trash2,
|
||||||
|
Plus,
|
||||||
|
RefreshCw,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Button } from "@/routes/ui/button";
|
||||||
|
import { ChainIcon } from "../components/shared/ChainIcon";
|
||||||
|
|
||||||
|
export interface Tweet {
|
||||||
|
number: number;
|
||||||
|
content: string;
|
||||||
|
type: "hook" | "analysis" | "implication" | "conclusion" | "disclaimer";
|
||||||
|
includeChart?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThreadRequest {
|
||||||
|
tokenAddress: string;
|
||||||
|
tokenSymbol: string;
|
||||||
|
chain: string;
|
||||||
|
topic?: string;
|
||||||
|
length: number;
|
||||||
|
tone: "bullish" | "neutral" | "bearish";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeneratedThread {
|
||||||
|
tweets: Tweet[];
|
||||||
|
metadata: {
|
||||||
|
tokenSymbol: string;
|
||||||
|
keyStats: Record<string, any>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThreadGeneratorPanelProps {
|
||||||
|
/** Current token info */
|
||||||
|
tokenAddress?: string;
|
||||||
|
tokenSymbol?: string;
|
||||||
|
chain?: string;
|
||||||
|
/** Callback when thread is generated */
|
||||||
|
onGenerate?: (request: ThreadRequest) => void;
|
||||||
|
/** Callback when thread is exported */
|
||||||
|
onExport?: (format: "copy" | "twitter") => void;
|
||||||
|
/** Additional class names */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThreadGeneratorPanel - AI-powered Twitter thread generator
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Auto-fill token info from current page
|
||||||
|
* - Customizable thread length (5-10 tweets)
|
||||||
|
* - Tone selection (bullish/neutral/bearish)
|
||||||
|
* - AI-generated thread structure (Hook → Analysis → Implications → Conclusion)
|
||||||
|
* - Edit individual tweets
|
||||||
|
* - Reorder tweets
|
||||||
|
* - Export options (copy all, tweet directly)
|
||||||
|
*/
|
||||||
|
export function ThreadGeneratorPanel({
|
||||||
|
tokenAddress,
|
||||||
|
tokenSymbol,
|
||||||
|
chain,
|
||||||
|
onGenerate,
|
||||||
|
onExport,
|
||||||
|
className,
|
||||||
|
}: ThreadGeneratorPanelProps) {
|
||||||
|
const [request, setRequest] = useState<ThreadRequest>({
|
||||||
|
tokenAddress: tokenAddress || "",
|
||||||
|
tokenSymbol: tokenSymbol || "",
|
||||||
|
chain: chain || "solana",
|
||||||
|
topic: "",
|
||||||
|
length: 7,
|
||||||
|
tone: "bullish",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [generatedThread, setGeneratedThread] = useState<GeneratedThread | null>(null);
|
||||||
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
|
const [editingTweet, setEditingTweet] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// Mock generated thread
|
||||||
|
const mockThread: GeneratedThread = {
|
||||||
|
tweets: [
|
||||||
|
{
|
||||||
|
number: 1,
|
||||||
|
content: `🧵 ${request.tokenSymbol} is showing massive volume spike (+200%) in the last 24h. Here's what you need to know 👇`,
|
||||||
|
type: "hook",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 2,
|
||||||
|
content: `Contract analysis:\n✅ Verified on-chain\n✅ Ownership renounced\n✅ LP locked for 90 days\n✅ No proxy contracts\n\nSolid fundamentals from a security perspective.`,
|
||||||
|
type: "analysis",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 3,
|
||||||
|
content: `Holder distribution looks healthy:\n• 1,234 holders\n• Top 10 hold only 35%\n• No single whale dominance\n\nThis suggests organic growth and reduced rug pull risk.`,
|
||||||
|
type: "analysis",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 4,
|
||||||
|
content: `Liquidity: $50K\nVolume/Liquidity ratio: 2.0x\n\nStrong trading activity relative to liquidity. This is a bullish signal for price discovery.`,
|
||||||
|
type: "analysis",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 5,
|
||||||
|
content: `Social sentiment is turning positive:\n• 500 Twitter mentions (24h)\n• 1,200 Telegram messages\n• Growing community engagement\n\nMomentum is building.`,
|
||||||
|
type: "implication",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 6,
|
||||||
|
content: `What this means:\n\nWe're seeing early signs of a potential breakout. Volume precedes price, and the fundamentals support sustained growth.`,
|
||||||
|
type: "implication",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 7,
|
||||||
|
content: `TL;DR:\n✅ Verified & safe contract\n✅ Healthy holder distribution\n✅ Strong volume growth\n✅ Positive social sentiment\n\nDYOR, but this one's worth watching closely. 👀`,
|
||||||
|
type: "conclusion",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
metadata: {
|
||||||
|
tokenSymbol: request.tokenSymbol,
|
||||||
|
keyStats: {
|
||||||
|
price: 0.0001234,
|
||||||
|
change24h: 15.5,
|
||||||
|
volume: 100000,
|
||||||
|
liquidity: 50000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGenerate = async () => {
|
||||||
|
setIsGenerating(true);
|
||||||
|
await onGenerate?.(request);
|
||||||
|
// Mock: simulate generation
|
||||||
|
setTimeout(() => {
|
||||||
|
setGeneratedThread(mockThread);
|
||||||
|
setIsGenerating(false);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditTweet = (number: number, newContent: string) => {
|
||||||
|
if (!generatedThread) return;
|
||||||
|
const updatedTweets = generatedThread.tweets.map((tweet) =>
|
||||||
|
tweet.number === number ? { ...tweet, content: newContent } : tweet
|
||||||
|
);
|
||||||
|
setGeneratedThread({ ...generatedThread, tweets: updatedTweets });
|
||||||
|
setEditingTweet(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteTweet = (number: number) => {
|
||||||
|
if (!generatedThread) return;
|
||||||
|
const updatedTweets = generatedThread.tweets
|
||||||
|
.filter((tweet) => tweet.number !== number)
|
||||||
|
.map((tweet, index) => ({ ...tweet, number: index + 1 }));
|
||||||
|
setGeneratedThread({ ...generatedThread, tweets: updatedTweets });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddTweet = () => {
|
||||||
|
if (!generatedThread) return;
|
||||||
|
const newTweet: Tweet = {
|
||||||
|
number: generatedThread.tweets.length + 1,
|
||||||
|
content: "New tweet content...",
|
||||||
|
type: "analysis",
|
||||||
|
};
|
||||||
|
setGeneratedThread({
|
||||||
|
...generatedThread,
|
||||||
|
tweets: [...generatedThread.tweets, newTweet],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-col h-full", className)}>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<MessageSquare className="h-5 w-5 text-primary" />
|
||||||
|
<div>
|
||||||
|
<h2 className="font-semibold">AI Thread Generator</h2>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Create Twitter threads with AI
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className=\"flex-1 overflow-y-auto p-4 space-y-4\">
|
||||||
|
{!generatedThread ? (
|
||||||
|
<>
|
||||||
|
{/* Input Form */}
|
||||||
|
<div className=\"space-y-3\">
|
||||||
|
{/* Token Info */}
|
||||||
|
<div className=\"space-y-2\">
|
||||||
|
<label className=\"text-sm font-medium\">Token</label>
|
||||||
|
<div className=\"flex items-center gap-2 p-2 bg-muted/50 rounded\">
|
||||||
|
<span className=\"font-semibold\">{request.tokenSymbol || \"Not selected\"}</span>
|
||||||
|
{request.chain && <ChainIcon chain={request.chain} size=\"xs\" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Topic */}
|
||||||
|
<div className=\"space-y-2\">
|
||||||
|
<label className=\"text-sm font-medium\">Topic (Optional)</label>
|
||||||
|
<input
|
||||||
|
type=\"text\"
|
||||||
|
placeholder=\"Auto-generate from token data\"
|
||||||
|
value={request.topic}
|
||||||
|
onChange={(e) => setRequest({ ...request, topic: e.target.value })}
|
||||||
|
className=\"w-full p-2 text-sm border rounded\"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Length */}
|
||||||
|
<div className=\"space-y-2\">
|
||||||
|
<label className=\"text-sm font-medium\">Length</label>
|
||||||
|
<select
|
||||||
|
value={request.length}
|
||||||
|
onChange={(e) => setRequest({ ...request, length: parseInt(e.target.value) })}
|
||||||
|
className=\"w-full p-2 text-sm border rounded\"
|
||||||
|
>
|
||||||
|
{[5, 6, 7, 8, 9, 10].map((len) => (
|
||||||
|
<option key={len} value={len}>
|
||||||
|
{len} tweets
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tone */}
|
||||||
|
<div className=\"space-y-2\">
|
||||||
|
<label className=\"text-sm font-medium\">Tone</label>
|
||||||
|
<div className=\"flex gap-2\">
|
||||||
|
{([\"bullish\", \"neutral\", \"bearish\"] as const).map((tone) => (
|
||||||
|
<button
|
||||||
|
key={tone}
|
||||||
|
className={cn(
|
||||||
|
\"flex-1 p-2 rounded border text-xs font-medium transition-colors\",
|
||||||
|
request.tone === tone
|
||||||
|
? \"bg-primary text-primary-foreground border-primary\"
|
||||||
|
: \"bg-muted hover:bg-muted/80\"
|
||||||
|
)}
|
||||||
|
onClick={() => setRequest({ ...request, tone })}
|
||||||
|
>
|
||||||
|
{tone.charAt(0).toUpperCase() + tone.slice(1)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Generate Button */}
|
||||||
|
<Button
|
||||||
|
variant=\"default\"
|
||||||
|
className=\"w-full\"
|
||||||
|
onClick={handleGenerate}
|
||||||
|
disabled={isGenerating || !request.tokenSymbol}
|
||||||
|
>
|
||||||
|
<Sparkles className=\"h-4 w-4 mr-2\" />
|
||||||
|
{isGenerating ? \"Generating...\" : \"Generate Thread\"}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Generated Thread Preview */}
|
||||||
|
<div className=\"space-y-3\">
|
||||||
|
<div className=\"flex items-center justify-between\">
|
||||||
|
<h3 className=\"font-semibold text-sm\">Preview</h3>
|
||||||
|
<Button
|
||||||
|
variant=\"ghost\"
|
||||||
|
size=\"sm\"
|
||||||
|
onClick={() => setGeneratedThread(null)}
|
||||||
|
>
|
||||||
|
<RefreshCw className=\"h-3 w-3 mr-1\" />
|
||||||
|
Regenerate
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tweets */}
|
||||||
|
<div className=\"space-y-2\">
|
||||||
|
{generatedThread.tweets.map((tweet) => (
|
||||||
|
<div
|
||||||
|
key={tweet.number}
|
||||||
|
className=\"p-3 border rounded-lg bg-background\"
|
||||||
|
>
|
||||||
|
{editingTweet === tweet.number ? (
|
||||||
|
<div className=\"space-y-2\">
|
||||||
|
<textarea
|
||||||
|
value={tweet.content}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleEditTweet(tweet.number, e.target.value)
|
||||||
|
}
|
||||||
|
className=\"w-full p-2 text-sm border rounded min-h-[80px]\"
|
||||||
|
/>
|
||||||
|
<div className=\"flex gap-2\">
|
||||||
|
<Button
|
||||||
|
variant=\"default\"
|
||||||
|
size=\"sm\"
|
||||||
|
onClick={() => setEditingTweet(null)}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant=\"outline\"
|
||||||
|
size=\"sm\"
|
||||||
|
onClick={() => setEditingTweet(null)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className=\"flex items-start justify-between mb-2\">
|
||||||
|
<span className=\"text-xs font-semibold text-muted-foreground\">
|
||||||
|
{tweet.number}/{generatedThread.tweets.length}
|
||||||
|
</span>
|
||||||
|
<div className=\"flex gap-1\">
|
||||||
|
<button
|
||||||
|
className=\"p-1 hover:bg-muted rounded\"
|
||||||
|
onClick={() => setEditingTweet(tweet.number)}
|
||||||
|
>
|
||||||
|
<Edit2 className=\"h-3 w-3\" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className=\"p-1 hover:bg-muted rounded\"
|
||||||
|
onClick={() => handleDeleteTweet(tweet.number)}
|
||||||
|
>
|
||||||
|
<Trash2 className=\"h-3 w-3\" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className=\"text-sm whitespace-pre-wrap\">{tweet.content}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Tweet Button */}
|
||||||
|
<Button
|
||||||
|
variant=\"outline\"
|
||||||
|
className=\"w-full\"
|
||||||
|
onClick={handleAddTweet}
|
||||||
|
>
|
||||||
|
<Plus className=\"h-4 w-4 mr-2\" />
|
||||||
|
Add Tweet
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer - Export Options */}
|
||||||
|
{generatedThread && (
|
||||||
|
<div className=\"border-t p-3 space-y-2\">
|
||||||
|
<Button
|
||||||
|
variant=\"default\"
|
||||||
|
className=\"w-full\"
|
||||||
|
onClick={() => onExport?.(\"copy\")}
|
||||||
|
>
|
||||||
|
<Copy className=\"h-4 w-4 mr-2\" />
|
||||||
|
Copy All Tweets
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant=\"outline\"
|
||||||
|
className=\"w-full\"
|
||||||
|
onClick={() => onExport?.(\"twitter\")}
|
||||||
|
>
|
||||||
|
<Twitter className=\"h-4 w-4 mr-2\" />
|
||||||
|
Tweet Now
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,374 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import {
|
||||||
|
Settings,
|
||||||
|
Bell,
|
||||||
|
BellOff,
|
||||||
|
Clock,
|
||||||
|
Keyboard,
|
||||||
|
Menu,
|
||||||
|
Save,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Button } from "@/routes/ui/button";
|
||||||
|
|
||||||
|
export interface NotificationSettings {
|
||||||
|
enabled: boolean;
|
||||||
|
priorities: {
|
||||||
|
high: boolean;
|
||||||
|
medium: boolean;
|
||||||
|
low: boolean;
|
||||||
|
};
|
||||||
|
quietHours: {
|
||||||
|
enabled: boolean;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
};
|
||||||
|
groupNotifications: boolean;
|
||||||
|
smartBatching: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeyboardShortcut {
|
||||||
|
id: string;
|
||||||
|
action: string;
|
||||||
|
shortcut: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuickActionsSettings {
|
||||||
|
contextMenuEnabled: boolean;
|
||||||
|
autoDetectAddresses: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductivitySettingsData {
|
||||||
|
notifications: NotificationSettings;
|
||||||
|
shortcuts: KeyboardShortcut[];
|
||||||
|
quickActions: QuickActionsSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductivitySettingsPanelProps {
|
||||||
|
/** Current settings */
|
||||||
|
settings?: ProductivitySettingsData;
|
||||||
|
/** Callback when settings are saved */
|
||||||
|
onSave?: (settings: ProductivitySettingsData) => void;
|
||||||
|
/** Additional class names */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProductivitySettingsPanel - Productivity settings management
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Notification settings (priorities, quiet hours, grouping)
|
||||||
|
* - Keyboard shortcuts configuration
|
||||||
|
* - Quick actions settings (context menu, auto-detect)
|
||||||
|
* - Per-token notification settings
|
||||||
|
*/
|
||||||
|
export function ProductivitySettingsPanel({
|
||||||
|
settings: initialSettings,
|
||||||
|
onSave,
|
||||||
|
className,
|
||||||
|
}: ProductivitySettingsPanelProps) {
|
||||||
|
const [settings, setSettings] = useState<ProductivitySettingsData>(
|
||||||
|
initialSettings || {
|
||||||
|
notifications: {
|
||||||
|
enabled: true,
|
||||||
|
priorities: {
|
||||||
|
high: true,
|
||||||
|
medium: true,
|
||||||
|
low: false,
|
||||||
|
},
|
||||||
|
quietHours: {
|
||||||
|
enabled: true,
|
||||||
|
start: "23:00",
|
||||||
|
end: "07:00",
|
||||||
|
},
|
||||||
|
groupNotifications: true,
|
||||||
|
smartBatching: true,
|
||||||
|
},
|
||||||
|
shortcuts: [
|
||||||
|
{ id: "open-panel", action: "Open Side Panel", shortcut: "Cmd+Shift+S", description: "Open/close the side panel" },
|
||||||
|
{ id: "new-chat", action: "New Chat", shortcut: "Cmd+Shift+N", description: "Start a new chat" },
|
||||||
|
{ id: "analyze-token", action: "Analyze Token", shortcut: "Cmd+Shift+A", description: "Analyze current token" },
|
||||||
|
{ id: "add-watchlist", action: "Add to Watchlist", shortcut: "Cmd+Shift+W", description: "Add token to watchlist" },
|
||||||
|
{ id: "capture-chart", action: "Capture Chart", shortcut: "Cmd+Shift+C", description: "Capture chart screenshot" },
|
||||||
|
{ id: "open-portfolio", action: "Open Portfolio", shortcut: "Cmd+Shift+P", description: "Open portfolio panel" },
|
||||||
|
],
|
||||||
|
quickActions: {
|
||||||
|
contextMenuEnabled: true,
|
||||||
|
autoDetectAddresses: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
|
||||||
|
const updateSettings = (updates: Partial<ProductivitySettingsData>) => {
|
||||||
|
setSettings({ ...settings, ...updates });
|
||||||
|
setHasChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onSave?.(settings);
|
||||||
|
setHasChanges(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-col h-full", className)}>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Settings className="h-5 w-5 text-primary" />
|
||||||
|
<div>
|
||||||
|
<h2 className="font-semibold">Productivity Settings</h2>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Notifications, shortcuts, and quick actions
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
||||||
|
{/* Notifications */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Bell className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<h3 className="font-semibold text-sm">Notifications</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Enable/Disable */}
|
||||||
|
<label className="flex items-center justify-between cursor-pointer">
|
||||||
|
<span className="text-sm">Enable notifications</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.notifications.enabled}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateSettings({
|
||||||
|
notifications: {
|
||||||
|
...settings.notifications,
|
||||||
|
enabled: e.target.checked,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Priority Levels */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-semibold text-muted-foreground">Priority Levels</label>
|
||||||
|
<div className="space-y-2 pl-4">
|
||||||
|
{(["high", "medium", "low"] as const).map((priority) => (
|
||||||
|
<label key={priority} className="flex items-center justify-between cursor-pointer">
|
||||||
|
<span className="text-sm capitalize">{priority}</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.notifications.priorities[priority]}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateSettings({
|
||||||
|
notifications: {
|
||||||
|
...settings.notifications,
|
||||||
|
priorities: {
|
||||||
|
...settings.notifications.priorities,
|
||||||
|
[priority]: e.target.checked,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="rounded"
|
||||||
|
disabled={!settings.notifications.enabled}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quiet Hours */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="flex items-center justify-between cursor-pointer">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="h-3 w-3 text-muted-foreground" />
|
||||||
|
<span className="text-sm">Quiet Hours</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.notifications.quietHours.enabled}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateSettings({
|
||||||
|
notifications: {
|
||||||
|
...settings.notifications,
|
||||||
|
quietHours: {
|
||||||
|
...settings.notifications.quietHours,
|
||||||
|
enabled: e.target.checked,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="rounded"
|
||||||
|
disabled={!settings.notifications.enabled}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{settings.notifications.quietHours.enabled && (
|
||||||
|
<div className="flex gap-2 pl-6">
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={settings.notifications.quietHours.start}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateSettings({
|
||||||
|
notifications: {
|
||||||
|
...settings.notifications,
|
||||||
|
quietHours: {
|
||||||
|
...settings.notifications.quietHours,
|
||||||
|
start: e.target.value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="flex-1 p-1 text-xs border rounded"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground">to</span>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={settings.notifications.quietHours.end}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateSettings({
|
||||||
|
notifications: {
|
||||||
|
...settings.notifications,
|
||||||
|
quietHours: {
|
||||||
|
...settings.notifications.quietHours,
|
||||||
|
end: e.target.value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="flex-1 p-1 text-xs border rounded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grouping Options */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="flex items-center justify-between cursor-pointer">
|
||||||
|
<span className="text-sm">Group notifications</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.notifications.groupNotifications}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateSettings({
|
||||||
|
notifications: {
|
||||||
|
...settings.notifications,
|
||||||
|
groupNotifications: e.target.checked,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="rounded"
|
||||||
|
disabled={!settings.notifications.enabled}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center justify-between cursor-pointer">
|
||||||
|
<span className="text-sm">Smart batching (5+ alerts)</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.notifications.smartBatching}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateSettings({
|
||||||
|
notifications: {
|
||||||
|
...settings.notifications,
|
||||||
|
smartBatching: e.target.checked,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="rounded"
|
||||||
|
disabled={!settings.notifications.enabled}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Keyboard Shortcuts */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Keyboard className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<h3 className="font-semibold text-sm">Keyboard Shortcuts</h3>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{settings.shortcuts.map((shortcut) => (
|
||||||
|
<div
|
||||||
|
key={shortcut.id}
|
||||||
|
className="flex items-center justify-between p-2 bg-muted/50 rounded"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium">{shortcut.action}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">{shortcut.description}</div>
|
||||||
|
</div>
|
||||||
|
<kbd className="px-2 py-1 text-xs font-mono bg-background border rounded">
|
||||||
|
{shortcut.shortcut}
|
||||||
|
</kbd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" className="w-full">
|
||||||
|
Customize Shortcuts
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Menu className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<h3 className="font-semibold text-sm">Quick Actions</h3>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="flex items-center justify-between cursor-pointer">
|
||||||
|
<span className="text-sm">Context menu enabled</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.quickActions.contextMenuEnabled}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateSettings({
|
||||||
|
quickActions: {
|
||||||
|
...settings.quickActions,
|
||||||
|
contextMenuEnabled: e.target.checked,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center justify-between cursor-pointer">
|
||||||
|
<span className="text-sm">Auto-detect token addresses</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.quickActions.autoDetectAddresses}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateSettings({
|
||||||
|
quickActions: {
|
||||||
|
...settings.quickActions,
|
||||||
|
autoDetectAddresses: e.target.checked,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer - Save Button */}
|
||||||
|
<div className="border-t p-3">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!hasChanges}
|
||||||
|
>
|
||||||
|
<Save className="h-4 w-4 mr-2" />
|
||||||
|
{hasChanges ? "Save Changes" : "No Changes"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue