diff --git a/frontend/src/components/PortfolioSetup.tsx b/frontend/src/components/PortfolioSetup.tsx index 0d5e454..029fce7 100644 --- a/frontend/src/components/PortfolioSetup.tsx +++ b/frontend/src/components/PortfolioSetup.tsx @@ -18,6 +18,8 @@ import { AlertCircle, CheckCircle, Search, + Save, + User, Zap, } from "lucide-react"; import { api, ENDPOINTS } from "../config/api"; @@ -50,6 +52,87 @@ const DEFAULT_ASSET_OPTIONS: AssetOption[] = [ { value: "ETH", label: "ETH (Ethereum)" }, ]; +export type RiskLevel = "low" | "medium" | "high"; + +export interface PortfolioTemplate { + id: string; + name: string; + description: string; + riskLevel: RiskLevel; + allocations: Allocation[]; +} + +export const PORTFOLIO_TEMPLATES: PortfolioTemplate[] = [ + { + id: "conservative", + name: "Conservative", + description: "Heavy on stablecoins and XLM. Lower volatility, capital preservation focus.", + riskLevel: "low", + allocations: [ + { asset: "USDC", percentage: 60 }, + { asset: "XLM", percentage: 30 }, + { asset: "BTC", percentage: 10 }, + ], + }, + { + id: "balanced", + name: "Balanced", + description: "Mix of stablecoins and crypto. Moderate risk with growth potential.", + riskLevel: "medium", + allocations: [ + { asset: "USDC", percentage: 40 }, + { asset: "XLM", percentage: 30 }, + { asset: "BTC", percentage: 20 }, + { asset: "ETH", percentage: 10 }, + ], + }, + { + id: "aggressive", + name: "Aggressive", + description: "Crypto-heavy for maximum growth. Higher volatility and risk.", + riskLevel: "high", + allocations: [ + { asset: "BTC", percentage: 50 }, + { asset: "ETH", percentage: 30 }, + { asset: "XLM", percentage: 20 }, + ], + }, + { + id: "stablecoin-focus", + name: "Stablecoin Focus", + description: "Mostly USDC with some XLM. Minimal exposure to crypto volatility.", + riskLevel: "low", + allocations: [ + { asset: "USDC", percentage: 80 }, + { asset: "XLM", percentage: 20 }, + ], + }, + { + id: "custom", + name: "Custom", + description: "Define your own allocation. Start from scratch and add assets.", + riskLevel: "medium", + allocations: [{ asset: "XLM", percentage: 100 }], + }, +]; + +const SAVED_TEMPLATES_KEY = (userId: string) => `portfolio-templates-${userId || "anonymous"}`; + +function loadSavedTemplates(userId: string): PortfolioTemplate[] { + try { + const raw = localStorage.getItem(SAVED_TEMPLATES_KEY(userId)); + if (!raw) return []; + const parsed = JSON.parse(raw) as PortfolioTemplate[]; + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +function saveSavedTemplates(userId: string, templates: PortfolioTemplate[]): void { + localStorage.setItem(SAVED_TEMPLATES_KEY(userId), JSON.stringify(templates)); +} + // ─── Component ──────────────────────────────────────────────────────────────── const PortfolioSetup: React.FC = ({ @@ -58,9 +141,10 @@ const PortfolioSetup: React.FC = ({ }) => { // ── State ────────────────────────────────────────────────────────────────── - const [allocations, setAllocations] = useState([ - { asset: "XLM", percentage: 40 }, - ]); + const [allocations, setAllocations] = useState(() => { + const balanced = PORTFOLIO_TEMPLATES.find((t) => t.id === "balanced"); + return balanced ? balanced.allocations.map((a) => ({ ...a })) : [{ asset: "XLM", percentage: 40 }]; + }); const [threshold, setThreshold] = useState(5); const [slippageTolerance, setSlippageTolerance] = useState(1); const [strategy, setStrategy] = useState("threshold"); @@ -71,6 +155,14 @@ const PortfolioSetup: React.FC = ({ const [isDemoMode] = useState(true); // demo mode: skips real wallet requirement const [assetOptions, setAssetOptions] = useState(DEFAULT_ASSET_OPTIONS); const [assetSearch, setAssetSearch] = useState>({}); // per-row filter for asset dropdown + const [selectedTemplateId, setSelectedTemplateId] = useState("balanced"); + const [savedTemplates, setSavedTemplates] = useState(() => + loadSavedTemplates(publicKey || "") + ); + + useEffect(() => { + setSavedTemplates(loadSavedTemplates(publicKey || "")); + }, [publicKey]); // ── Fetch available assets from registry (dynamic, supports custom Stellar tokens) ── useEffect(() => { @@ -91,6 +183,23 @@ const PortfolioSetup: React.FC = ({ return () => { cancelled = true; }; }, []); + const getRiskLevelLabel = (level: RiskLevel): string => { + switch (level) { + case "low": return "Low risk"; + case "medium": return "Medium risk"; + case "high": return "High risk"; + default: return "Risk"; + } + }; + + const getRiskLevelClass = (level: RiskLevel): string => { + switch (level) { + case "low": return "bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300"; + case "medium": return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/40 dark:text-yellow-300"; + case "high": return "bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300"; + default: return "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300"; + } + }; // Mutation for portfolio creation const createPortfolioMutation = useCreatePortfolioMutation(); @@ -234,9 +343,40 @@ const PortfolioSetup: React.FC = ({ setAllocations(updated); }; - /** Replaces the current allocation list with a preset configuration */ - const applyPreset = (preset: (typeof presetPortfolios)[0]) => { - setAllocations(preset.allocations); + /** Replaces the current allocation list with a template. User can modify before creating. */ + const applyTemplate = (template: PortfolioTemplate) => { + setSelectedTemplateId(template.id); + setAllocations(template.allocations.map((a) => ({ ...a }))); + }; + + const saveCurrentAsTemplate = () => { + const name = window.prompt("Template name", "My custom template"); + if (!name?.trim()) return; + if (!isValidTotal || hasAnyFieldError) return; + const custom: PortfolioTemplate = { + id: `saved-${Date.now()}`, + name: name.trim(), + description: "Saved by you. Modify and use as a starting point.", + riskLevel: "medium", + allocations: allocations.map((a) => ({ ...a })), + }; + const userId = publicKey || ""; + const next = [...savedTemplates, custom]; + setSavedTemplates(next); + saveSavedTemplates(userId, next); + setSelectedTemplateId(custom.id); + }; + + const removeSavedTemplate = (id: string) => { + if (!window.confirm("Remove this saved template?")) return; + const userId = publicKey || ""; + const next = savedTemplates.filter((t) => t.id !== id); + setSavedTemplates(next); + saveSavedTemplates(userId, next); + if (selectedTemplateId === id) { + setSelectedTemplateId("custom"); + setAllocations([{ asset: "XLM", percentage: 100 }]); + } }; /** @@ -366,25 +506,106 @@ const PortfolioSetup: React.FC = ({
{/* ════ Left column: configuration inputs ════ */}
- {/* ── Preset portfolio quick-start buttons ── */} + {/* ── Template selector: presets with descriptions and risk levels ── */}
-

- Quick Start +

+ Choose a template

-
- {presetPortfolios.map((preset, index) => ( +

+ Start from a preset or custom. You can modify allocations below before creating. +

+
+ {PORTFOLIO_TEMPLATES.map((template) => ( ))}
+ {savedTemplates.length > 0 && ( + <> +

+ + My saved templates +

+
+ {savedTemplates.map((template) => ( +
+
+ + +
+
+ ))} +
+ + )} + {isValidTotal && !hasAnyFieldError && ( +
+ +
+ )}
{/* ── Asset allocation rows with inline validation ── */}