Загрузка...

Адаптивная дашборд-карточка с таймлайном инвестиций. Фильтры, сортировка, выбор периода, модальное окно добавления. Идеально для финансов.
<html lang="en"><head><meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Portfolio Manager — Timeline</title>
<!-- Inter font -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<!-- Tailwind CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script></head>
<body class="bg-[#0b0f14] text-neutral-200 antialiased min-h-screen flex items-center justify-center p-4 sm:p-6" style="font-family:'Inter', ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica Arial">
<!-- Single Card Container -->
<div class="w-full max-w-3xl min-h-[400px] rounded-2xl sm:rounded-3xl border border-white/10 bg-white/[0.03] backdrop-blur-sm shadow-[0_20px_50px_-10px_rgba(0,0,0,0.6)]">
<!-- Card Header -->
<div class="flex flex-col sm:flex-row items-start justify-between gap-4 p-4 sm:p-6 lg:p-8 pb-4 sm:pb-6">
<div>
<h1 class="text-xl sm:text-2xl lg:text-3xl font-semibold tracking-tight">Investment Timeline</h1>
<p class="mt-1 text-sm text-neutral-400" id="period-subtitle">Q4 portfolio overview</p>
</div>
<div class="flex items-center gap-2 sm:gap-3 w-full sm:w-auto">
<!-- View range button -->
<div class="relative flex-1 sm:flex-none">
<button id="period-btn" class="w-full sm:w-auto inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 sm:px-3.5 py-2.5 text-sm text-neutral-200 hover:bg-white/[0.08] transition">
<i data-lucide="calendar" class="h-4 w-4 text-neutral-300"></i>
<span class="font-medium" id="period-text">Quarter</span>
<i data-lucide="chevron-down" class="h-4 w-4 text-neutral-400"></i>
</button>
<!-- Dropdown -->
<div id="period-dropdown" class="absolute top-full mt-2 right-0 w-48 rounded-xl border border-white/10 bg-[#0b0f14] backdrop-blur-sm shadow-[0_20px_50px_-10px_rgba(0,0,0,0.6)] z-20 hidden">
<div class="p-2">
<button class="period-option w-full px-3 py-2 text-left text-sm rounded-lg hover:bg-white/[0.06] transition" data-period="week">Week</button>
<button class="period-option w-full px-3 py-2 text-left text-sm rounded-lg hover:bg-white/[0.06] transition" data-period="month">Month</button>
<button class="period-option w-full px-3 py-2 text-left text-sm rounded-lg hover:bg-white/[0.06] transition bg-white/[0.06]" data-period="quarter">Quarter</button>
<button class="period-option w-full px-3 py-2 text-left text-sm rounded-lg hover:bg-white/[0.06] transition" data-period="year">Year</button>
</div>
</div>
</div>
<!-- New Investment -->
<button id="new-investment-btn" class="inline-flex items-center gap-2 rounded-xl px-3 sm:px-4 py-2.5 text-sm font-medium text-white shadow-[0_6px_20px_-6px_rgba(59,130,246,0.5)] bg-gradient-to-b from-[#1f78ff] to-[#1766e8] hover:from-[#2a82ff] hover:to-[#1a6ef1] transition whitespace-nowrap">
<i data-lucide="plus" class="h-4 w-4"></i>
<span class="hidden xs:inline">New Investment</span>
<span class="xs:hidden">New</span>
</button>
</div>
</div>
<!-- Divider -->
<div class="mx-4 sm:mx-6 lg:mx-8 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent"></div>
<!-- Navigation Tabs -->
<div class="px-4 sm:px-6 lg:px-8 pt-4 sm:pt-6">
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<nav class="flex gap-1 w-full sm:w-auto overflow-x-auto">
<button class="nav-tab relative px-3.5 py-2 text-sm text-neutral-300 hover:text-white rounded-lg hover:bg-white/[0.06] transition whitespace-nowrap" data-tab="portfolio">Portfolio</button>
<button class="nav-tab relative px-3.5 py-2 text-sm rounded-lg text-white whitespace-nowrap" data-tab="timeline">
<span class="relative z-10 font-medium">Timeline</span>
<span class="absolute inset-0 rounded-lg bg-white/[0.06]"></span>
<span class="absolute left-1/2 top-full h-[2px] w-10 -translate-x-1/2 bg-gradient-to-r from-transparent via-[#2a7fff] to-transparent"></span>
</button>
<button class="nav-tab relative px-3.5 py-2 text-sm text-neutral-300 hover:text-white rounded-lg hover:bg-white/[0.06] transition whitespace-nowrap" data-tab="analytics">Analytics</button>
<button class="nav-tab relative px-3.5 py-2 text-sm text-neutral-300 hover:text-white rounded-lg hover:bg-white/[0.06] transition whitespace-nowrap" data-tab="holdings">Holdings</button>
</nav>
<!-- Right toolbar -->
<div class="flex items-center gap-2">
<div class="relative">
<button id="filter-btn" class="inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-neutral-300 hover:text-white hover:bg-white/[0.06] transition">
<i data-lucide="filter" class="h-4 w-4"></i>
<span class="hidden sm:inline">Filter</span>
<span id="filter-count" class="hidden ml-1 px-1.5 py-0.5 text-xs bg-[#2a7fff] text-white rounded-md">2</span>
</button>
<!-- Filter dropdown -->
<div id="filter-dropdown" class="absolute top-full mt-2 right-0 w-56 rounded-xl border border-white/10 bg-[#0b0f14] backdrop-blur-sm shadow-[0_20px_50px_-10px_rgba(0,0,0,0.6)] z-20 hidden">
<div class="p-4">
<div class="mb-3">
<label class="block text-xs font-medium text-neutral-300 mb-2">Investment Type</label>
<div class="space-y-2">
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" class="filter-checkbox rounded border-white/20 bg-white/5 text-[#2a7fff]" data-filter="analysis" checked="">
<span>Market Analysis</span>
</label>
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" class="filter-checkbox rounded border-white/20 bg-white/5 text-[#2a7fff]" data-filter="esg" checked="">
<span>ESG Screening</span>
</label>
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" class="filter-checkbox rounded border-white/20 bg-white/5 text-[#2a7fff]" data-filter="derivatives">
<span>Derivatives</span>
</label>
</div>
</div>
<div class="pt-3 border-t border-white/10">
<button id="clear-filters" class="text-xs text-[#2a7fff] hover:text-blue-300 transition">Clear all</button>
</div>
</div>
</div>
</div>
<div class="relative">
<button id="sort-btn" class="inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-neutral-300 hover:text-white hover:bg-white/[0.06] transition">
<i data-lucide="sort-desc" class="h-4 w-4"></i>
<span class="hidden sm:inline">Sort</span>
</button>
<!-- Sort dropdown -->
<div id="sort-dropdown" class="absolute top-full mt-2 right-0 w-48 rounded-xl border border-white/10 bg-[#0b0f14] backdrop-blur-sm shadow-[0_20px_50px_-10px_rgba(0,0,0,0.6)] z-20 hidden">
<div class="p-2">
<button class="sort-option w-full px-3 py-2 text-left text-sm rounded-lg hover:bg-white/[0.06] transition bg-white/[0.06]" data-sort="date">Date</button>
<button class="sort-option w-full px-3 py-2 text-left text-sm rounded-lg hover:bg-white/[0.06] transition" data-sort="amount">Amount</button>
<button class="sort-option w-full px-3 py-2 text-left text-sm rounded-lg hover:bg-white/[0.06] transition" data-sort="type">Type</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tab Content Container - Responsive height -->
<div class="mt-4 sm:mt-6 min-h-[350px] sm:min-h-[400px]">
<!-- Portfolio Tab Content -->
<div class="tab-content hidden" data-content="portfolio">
<div class="px-4 sm:px-6 lg:px-8">
<div class="rounded-xl sm:rounded-2xl border border-white/10 bg-white/[0.02] p-6 sm:p-8">
<div class="text-center py-8 sm:py-12">
<i data-lucide="briefcase" class="h-10 sm:h-12 w-10 sm:w-12 text-neutral-400 mx-auto mb-4"></i>
<h3 class="text-lg font-semibold text-neutral-200 mb-2">Portfolio Overview</h3>
<p class="text-sm text-neutral-400 mb-6 max-w-sm mx-auto">View your complete investment portfolio performance and allocation.</p>
<button class="inline-flex items-center gap-2 rounded-xl px-4 py-2.5 text-sm font-medium text-white bg-gradient-to-b from-[#1f78ff] to-[#1766e8] hover:from-[#2a82ff] hover:to-[#1a6ef1] transition">
<i data-lucide="pie-chart" class="h-4 w-4"></i>
View Portfolio
</button>
</div>
</div>
</div>
</div>
<!-- Timeline Tab Content (Active) -->
<div class="tab-content" data-content="timeline">
<div class="px-4 sm:px-6 lg:px-8">
<!-- Navigation arrows -->
<div class="flex items-center justify-between mb-4">
<button id="prev-period" class="inline-flex items-center gap-1 sm:gap-2 rounded-lg px-2 sm:px-3 py-2 text-sm text-neutral-300 hover:text-white hover:bg-white/[0.06] transition">
<i data-lucide="chevron-left" class="h-4 w-4"></i>
<span class="hidden sm:inline">Previous</span>
</button>
<div class="text-xs sm:text-sm text-neutral-400 text-center" id="current-period">October - November 2023</div>
<button id="next-period" class="inline-flex items-center gap-1 sm:gap-2 rounded-lg px-2 sm:px-3 py-2 text-sm text-neutral-300 hover:text-white hover:bg-white/[0.06] transition">
<span class="hidden sm:inline">Next</span>
<i data-lucide="chevron-right" class="h-4 w-4"></i>
</button>
</div>
<!-- Scroll container -->
<div class="relative overflow-x-auto rounded-xl sm:rounded-2xl border border-white/10 bg-white/[0.02]">
<!-- This inner wrapper controls column width -->
<div class="min-w-[600px] sm:min-w-[900px]" id="timeline-container">
<!-- Header rows -->
<div id="timeline-header" class="border-b border-white/10">
<!-- Dynamic header content will be inserted here -->
</div>
<!-- Grid + Tasks -->
<div class="relative">
<!-- Column grid background -->
<div id="timeline-grid">
<!-- Dynamic grid columns will be inserted here -->
</div>
<!-- Current day marker -->
<div id="current-day-marker" class="pointer-events-none absolute top-0 bottom-0 w-px bg-gradient-to-b from-transparent via-[#2a7fff] to-transparent hidden">
</div>
<!-- Tasks layer -->
<div class="absolute inset-0" id="tasks-container">
<!-- Dynamic tasks will be inserted here -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Analytics Tab Content -->
<div class="tab-content hidden" data-content="analytics">
<div class="px-4 sm:px-6 lg:px-8">
<div class="rounded-xl sm:rounded-2xl border border-white/10 bg-white/[0.02] p-6 sm:p-8">
<div class="text-center py-8 sm:py-12">
<i data-lucide="bar-chart-3" class="h-10 sm:h-12 w-10 sm:w-12 text-neutral-400 mx-auto mb-4"></i>
<h3 class="text-lg font-semibold text-neutral-200 mb-2">Analytics Dashboard</h3>
<p class="text-sm text-neutral-400 mb-6 max-w-sm mx-auto">Deep dive into performance metrics, risk analysis, and market insights.</p>
<button class="inline-flex items-center gap-2 rounded-xl px-4 py-2.5 text-sm font-medium text-white bg-gradient-to-b from-[#1f78ff] to-[#1766e8] hover:from-[#2a82ff] hover:to-[#1a6ef1] transition">
<i data-lucide="trending-up" class="h-4 w-4"></i>
View Analytics
</button>
</div>
</div>
</div>
</div>
<!-- Holdings Tab Content -->
<div class="tab-content hidden" data-content="holdings">
<div class="px-4 sm:px-6 lg:px-8">
<div class="rounded-xl sm:rounded-2xl border border-white/10 bg-white/[0.02] p-6 sm:p-8">
<div class="text-center py-8 sm:py-12">
<i data-lucide="layers" class="h-10 sm:h-12 w-10 sm:w-12 text-neutral-400 mx-auto mb-4"></i>
<h3 class="text-lg font-semibold text-neutral-200 mb-2">Current Holdings</h3>
<p class="text-sm text-neutral-400 mb-6 max-w-sm mx-auto">Detailed view of all your current positions and asset allocation.</p>
<button class="inline-flex items-center gap-2 rounded-xl px-4 py-2.5 text-sm font-medium text-white bg-gradient-to-b from-[#1f78ff] to-[#1766e8] hover:from-[#2a82ff] hover:to-[#1a6ef1] transition">
<i data-lucide="list" class="h-4 w-4"></i>
View Holdings
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- New Investment Modal -->
<div id="investment-modal" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4 sm:p-6 hidden">
<div class="w-full max-w-md rounded-xl sm:rounded-2xl border border-white/10 bg-[#0b0f14] shadow-[0_20px_50px_-10px_rgba(0,0,0,0.8)] max-h-[90vh] overflow-y-auto">
<div class="p-4 sm:p-6">
<div class="flex items-center justify-between mb-4 sm:mb-6">
<h2 class="text-lg sm:text-xl font-semibold tracking-tight">New Investment</h2>
<button id="close-modal" class="text-neutral-400 hover:text-white">
<i data-lucide="x" class="h-5 w-5"></i>
</button>
</div>
<form id="investment-form" class="space-y-4">
<div>
<label class="block text-sm font-medium text-neutral-300 mb-2">Title</label>
<input type="text" id="investment-title" class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-neutral-400 focus:outline-none focus:border-[#2a7fff] text-sm" placeholder="Investment title">
</div>
<div>
<label class="block text-sm font-medium text-neutral-300 mb-2">Type</label>
<select id="investment-type" class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:border-[#2a7fff] text-sm">
<option value="analysis">Market Analysis</option>
<option value="esg">ESG Screening</option>
<option value="derivatives">Derivatives</option>
</select>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-neutral-300 mb-2">Start Date</label>
<input type="date" id="investment-start" class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:border-[#2a7fff] text-sm">
</div>
<div>
<label class="block text-sm font-medium text-neutral-300 mb-2">End Date</label>
<input type="date" id="investment-end" class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:border-[#2a7fff] text-sm">
</div>
</div>
<div>
<label class="block text-sm font-medium text-neutral-300 mb-2">Amount</label>
<input type="text" id="investment-amount" class="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-neutral-400 focus:outline-none focus:border-[#2a7fff] text-sm" placeholder="$0.00">
</div>
<div class="flex gap-3 pt-4">
<button type="button" id="cancel-investment" class="flex-1 px-4 py-2.5 text-sm font-medium text-neutral-300 border border-white/10 rounded-xl hover:bg-white/[0.06] transition">
Cancel
</button>
<button type="submit" class="flex-1 px-4 py-2.5 text-sm font-medium text-white bg-gradient-to-b from-[#1f78ff] to-[#1766e8] hover:from-[#2a82ff] hover:to-[#1a6ef1] rounded-xl transition shadow-[0_6px_20px_-6px_rgba(59,130,246,0.5)]">
Add Investment
</button>
</div>
</form>
</div>
</div>
</div>
<script>
// Add responsive breakpoint configuration to Tailwind
tailwind.config = {
theme: {
extend: {
screens: {
'xs': '475px',
}
}
}
}
// Current date for timeline calculations
const currentDate = new Date();
// Investment data with different time ranges
const investmentData = {
week: [
{
id: 1,
title: 'Daily Market Analysis',
type: 'analysis',
startDate: new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() - 2),
endDate: new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 1),
amount: 850000,
color: 'from-[#2a7fff] to-[#0ea5e9]',
icon: 'trending-up',
teamSize: 2
},
{
id: 2,
title: 'ESG Quick Screen',
type: 'esg',
startDate: new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate()),
endDate: new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 2),
amount: 425000,
color: 'from-[#22d3ee] to-[#14b8a6]',
icon: 'leaf',
teamSize: 1
}
],
month: [
{
id: 1,
title: 'Monthly Portfolio Review',
type: 'analysis',
startDate: new Date(currentDate.getFullYear(), currentDate.getMonth(), 5),
endDate: new Date(currentDate.getFullYear(), currentDate.getMonth(), 12),
amount: 1850000,
color: 'from-[#2a7fff] to-[#0ea5e9]',
icon: 'trending-up',
teamSize: 4
},
{
id: 2,
title: 'ESG Compliance Audit',
type: 'esg',
startDate: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8),
endDate: new Date(currentDate.getFullYear(), currentDate.getMonth(), 18),
amount: 1200000,
color: 'from-[#22d3ee] to-[#14b8a6]',
icon: 'leaf',
teamSize: 3
},
{
id: 3,
title: 'Options Strategy',
type: 'derivatives',
startDate: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15),
endDate: new Date(currentDate.getFullYear(), currentDate.getMonth(), 28),
amount: 3200000,
color: 'from-[#a78bfa] to-[#7c3aed]',
icon: 'shield',
teamSize: 5
}
],
quarter: [
{
id: 1,
title: 'Market Analysis',
type: 'analysis',
startDate: new Date(2023, 9, 28),
endDate: new Date(2023, 10, 2),
amount: 2500000,
color: 'from-[#2a7fff] to-[#0ea5e9]',
icon: 'trending-up',
teamSize: 3
},
{
id: 2,
title: 'ESG Screening',
type: 'esg',
startDate: new Date(2023, 9, 29),
endDate: new Date(2023, 10, 3),
amount: 1800000,
color: 'from-[#22d3ee] to-[#14b8a6]',
icon: 'leaf',
teamSize: 2
},
{
id: 3,
title: 'Derivatives Hedge',
type: 'derivatives',
startDate: new Date(2023, 9, 31),
endDate: new Date(2023, 10, 6),
amount: 4200000,
color: 'from-[#a78bfa] to-[#7c3aed]',
icon: 'shield',
teamSize: 4
}
],
year: [
{
id: 1,
title: 'Annual Strategy Review',
type: 'analysis',
startDate: new Date(2023, 1, 15),
endDate: new Date(2023, 4, 30),
amount: 8500000,
color: 'from-[#2a7fff] to-[#0ea5e9]',
icon: 'trending-up',
teamSize: 8
},
{
id: 2,
title: 'ESG Transformation',
type: 'esg',
startDate: new Date(2023, 3, 1),
endDate: new Date(2023, 8, 15),
amount: 12000000,
color: 'from-[#22d3ee] to-[#14b8a6]',
icon: 'leaf',
teamSize: 12
},
{
id: 3,
title: 'Risk Management Overhaul',
type: 'derivatives',
startDate: new Date(2023, 6, 1),
endDate: new Date(2023, 11, 31),
amount: 15500000,
color: 'from-[#a78bfa] to-[#7c3aed]',
icon: 'shield',
teamSize: 15
}
]
};
// Current view state
let currentPeriod = 'quarter';
let currentSort = 'date';
let activeFilters = ['analysis', 'esg'];
let activeTab = 'timeline';
let currentTimelineData = {};
// Initialize app
document.addEventListener('DOMContentLoaded', () => {
if (window.lucide) lucide.createIcons();
initializeEventListeners();
updateTimelineData();
renderTimeline();
updateStats();
});
function initializeEventListeners() {
// Period dropdown
document.getElementById('period-btn').addEventListener('click', (e) => {
e.stopPropagation();
toggleDropdown('period-dropdown');
});
document.querySelectorAll('.period-option').forEach(btn => {
btn.addEventListener('click', (e) => {
// Update active state
document.querySelectorAll('.period-option').forEach(b => b.classList.remove('bg-white/[0.06]'));
e.target.classList.add('bg-white/[0.06]');
currentPeriod = e.target.dataset.period;
document.getElementById('period-text').textContent = currentPeriod.charAt(0).toUpperCase() + currentPeriod.slice(1);
updatePeriodSubtitle();
updateTimelineData();
renderTimeline();
updateStats();
hideDropdown('period-dropdown');
});
});
// Filter dropdown
document.getElementById('filter-btn').addEventListener('click', (e) => {
e.stopPropagation();
toggleDropdown('filter-dropdown');
});
document.querySelectorAll('.filter-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const filter = e.target.dataset.filter;
if (e.target.checked) {
if (!activeFilters.includes(filter)) {
activeFilters.push(filter);
}
} else {
activeFilters = activeFilters.filter(f => f !== filter);
}
updateFilterCount();
renderTimeline();
});
});
document.getElementById('clear-filters').addEventListener('click', () => {
activeFilters = [];
document.querySelectorAll('.filter-checkbox').forEach(cb => cb.checked = false);
updateFilterCount();
renderTimeline();
});
// Sort dropdown
document.getElementById('sort-btn').addEventListener('click', (e) => {
e.stopPropagation();
toggleDropdown('sort-dropdown');
});
document.querySelectorAll('.sort-option').forEach(btn => {
btn.addEventListener('click', (e) => {
currentSort = e.target.dataset.sort;
document.querySelectorAll('.sort-option').forEach(b => b.classList.remove('bg-white/[0.06]'));
e.target.classList.add('bg-white/[0.06]');
renderTimeline();
hideDropdown('sort-dropdown');
});
});
// Navigation tabs
document.querySelectorAll('.nav-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
const tabName = e.currentTarget.dataset.tab;
switchTab(tabName);
});
});
// Timeline navigation
document.getElementById('prev-period').addEventListener('click', () => {
console.log('Previous period clicked');
});
document.getElementById('next-period').addEventListener('click', () => {
console.log('Next period clicked');
});
// New investment modal
document.getElementById('new-investment-btn').addEventListener('click', () => {
document.getElementById('investment-modal').classList.remove('hidden');
const today = new Date();
document.getElementById('investment-start').valueAsDate = today;
document.getElementById('investment-end').valueAsDate = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000);
});
document.getElementById('close-modal').addEventListener('click', closeModal);
document.getElementById('cancel-investment').addEventListener('click', closeModal);
document.getElementById('investment-form').addEventListener('submit', (e) => {
e.preventDefault();
addNewInvestment();
});
// Close dropdowns when clicking outside
document.addEventListener('click', () => {
hideDropdown('period-dropdown');
hideDropdown('filter-dropdown');
hideDropdown('sort-dropdown');
});
// Close modal when clicking backdrop
document.getElementById('investment-modal').addEventListener('click', (e) => {
if (e.target.id === 'investment-modal') {
closeModal();
}
});
}
function updateTimelineData() {
currentTimelineData = getTimelineConfig(currentPeriod);
}
function getTimelineConfig(period) {
const now = new Date();
switch (period) {
case 'week':
const weekStart = new Date(now);
weekStart.setDate(now.getDate() - now.getDay()); // Start of week (Sunday)
return {
investments: investmentData.week,
columns: 7,
startDate: weekStart,
getColumnLabel: (index) => {
const date = new Date(weekStart);
date.setDate(weekStart.getDate() + index);
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return `${date.getDate()} ${dayNames[date.getDay()]}`;
},
getPeriodLabel: () => {
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
return `${weekStart.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${weekEnd.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`;
},
getCurrentMarkerPosition: () => {
const dayOfWeek = now.getDay();
return `calc((100% / 7) * ${dayOfWeek} + (100% / 14))`;
},
showCurrentMarker: true
};
case 'month':
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
const columns = Math.min(daysInMonth, 31);
return {
investments: investmentData.month,
columns: columns,
startDate: monthStart,
getColumnLabel: (index) => {
return `${index + 1}`;
},
getPeriodLabel: () => {
return now.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
},
getCurrentMarkerPosition: () => {
const currentDay = now.getDate();
return `calc((100% / ${columns}) * ${currentDay - 1} + (100% / ${columns * 2}))`;
},
showCurrentMarker: true
};
case 'quarter':
return {
investments: investmentData.quarter,
columns: 14,
startDate: new Date(2023, 9, 27),
getColumnLabel: (index) => {
const dates = [
'27 F', '28 S', '29 S', '30 M', '31 T', '1 W', '2 T',
'3 F', '4 S', '5 S', '6 M', '7 T', '8 W', '9 T'
];
return dates[index] || '';
},
getPeriodLabel: () => 'October - November 2023',
getCurrentMarkerPosition: () => 'calc((100% / 14) * 4)',
showCurrentMarker: false
};
case 'year':
return {
investments: investmentData.year,
columns: 12,
startDate: new Date(2023, 0, 1),
getColumnLabel: (index) => {
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return monthNames[index] || '';
},
getPeriodLabel: () => '2023',
getCurrentMarkerPosition: () => {
const currentMonth = now.getMonth();
return `calc((100% / 12) * ${currentMonth} + (100% / 24))`;
},
showCurrentMarker: true
};
default:
return getTimelineConfig('quarter');
}
}
function switchTab(tabName) {
activeTab = tabName;
document.querySelectorAll('.nav-tab').forEach(tab => {
const isActive = tab.dataset.tab === tabName;
const tabText = tab.dataset.tab.charAt(0).toUpperCase() + tab.dataset.tab.slice(1);
if (isActive) {
tab.className = 'nav-tab relative px-3.5 py-2 text-sm rounded-lg text-white whitespace-nowrap';
tab.innerHTML = `
<span class="relative z-10 font-medium">${tabText}</span>
<span class="absolute inset-0 rounded-lg bg-white/[0.06]"></span>
<span class="absolute left-1/2 top-full h-[2px] w-10 -translate-x-1/2 bg-gradient-to-r from-transparent via-[#2a7fff] to-transparent"></span>
`;
} else {
tab.className = 'nav-tab relative px-3.5 py-2 text-sm text-neutral-300 hover:text-white rounded-lg hover:bg-white/[0.06] transition whitespace-nowrap';
tab.innerHTML = tabText;
}
});
document.querySelectorAll('.tab-content').forEach(content => {
if (content.dataset.content === tabName) {
content.classList.remove('hidden');
} else {
content.classList.add('hidden');
}
});
if (tabName === 'timeline') {
renderTimeline();
}
if (window.lucide) lucide.createIcons();
}
function toggleDropdown(id) {
const dropdown = document.getElementById(id);
dropdown.classList.toggle('hidden');
}
function hideDropdown(id) {
document.getElementById(id).classList.add('hidden');
}
function updatePeriodSubtitle() {
const subtitles = {
week: 'Weekly portfolio overview',
month: 'Monthly portfolio overview',
quarter: 'Q4 portfolio overview',
year: 'Annual portfolio overview'
};
document.getElementById('period-subtitle').textContent = subtitles[currentPeriod];
}
function updateFilterCount() {
const count = activeFilters.length;
const countElement = document.getElementById('filter-count');
if (count > 0) {
countElement.textContent = count;
countElement.classList.remove('hidden');
} else {
countElement.classList.add('hidden');
}
}
function renderTimeline() {
renderTimelineHeader();
renderTimelineGrid();
renderTasks();
updateCurrentMarker();
}
function renderTimelineHeader() {
const header = document.getElementById('timeline-header');
const { columns, getColumnLabel, getPeriodLabel } = currentTimelineData;
// Update period label
document.getElementById('current-period').textContent = getPeriodLabel();
let headerHTML = '';
if (currentPeriod === 'quarter') {
// Special layout for quarter view with month headers
headerHTML = `
<div class="grid [grid-template-columns:repeat(${columns},minmax(60px,1fr))] sm:[grid-template-columns:repeat(${columns},minmax(80px,1fr))]">
<div class="col-[1/8] px-2 sm:px-4 py-3 text-sm text-neutral-400 font-medium">October</div>
<div class="col-[8/15] px-2 sm:px-4 py-3 text-sm text-neutral-400 font-medium">November</div>
</div>
<div class="grid [grid-template-columns:repeat(${columns},minmax(60px,1fr))] sm:[grid-template-columns:repeat(${columns},minmax(80px,1fr))] text-[10px] sm:text-[11px] uppercase tracking-wide text-neutral-400/90">
${Array.from({ length: columns }, (_, i) =>
`<div class="px-2 sm:px-4 py-2 border-l border-white/5">${getColumnLabel(i)}</div>`
).join('')}
</div>
`;
} else {
// Single row header for other views
headerHTML = `
<div class="grid [grid-template-columns:repeat(${columns},minmax(60px,1fr))] sm:[grid-template-columns:repeat(${columns},minmax(80px,1fr))] text-[10px] sm:text-[11px] uppercase tracking-wide text-neutral-400/90">
${Array.from({ length: columns }, (_, i) =>
`<div class="px-2 sm:px-4 py-2 border-l border-white/5">${getColumnLabel(i)}</div>`
).join('')}
</div>
`;
}
header.innerHTML = headerHTML;
}
function renderTimelineGrid() {
const grid = document.getElementById('timeline-grid');
const { columns } = currentTimelineData;
let gridHTML = `<div class="grid [grid-template-columns:repeat(${columns},minmax(60px,1fr))] sm:[grid-template-columns:repeat(${columns},minmax(80px,1fr))]">`;
for (let i = 0; i < columns; i++) {
const bgClass = i % 2 === 1 ? 'bg-white/[0.01]' : '';
const borderClass = i === columns - 1 ? 'border-r' : '';
gridHTML += `<div class="h-[220px] sm:h-[250px] border-l border-white/[0.05] ${bgClass} ${borderClass}"></div>`;
}
gridHTML += '</div>';
grid.innerHTML = gridHTML;
}
function renderTasks() {
const container = document.getElementById('tasks-container');
const filteredInvestments = getFilteredInvestments();
const { columns } = currentTimelineData;
let tasksHTML = `<div class="grid [grid-template-columns:repeat(${columns},minmax(60px,1fr))] sm:[grid-template-columns:repeat(${columns},minmax(80px,1fr))] gap-y-3 sm:gap-y-4 p-2 sm:p-4">`;
filteredInvestments.forEach((investment, index) => {
const startCol = getColumnForDate(investment.startDate);
const endCol = getColumnForDate(investment.endDate);
tasksHTML += `
<div class="col-[1/${columns + 1}] h-1"></div>
<div class="col-[${startCol}/${endCol + 1}]">
<div class="relative flex items-center justify-between rounded-lg sm:rounded-xl border border-white/10 bg-white/[0.06] px-2 sm:px-3 py-2 sm:py-2.5 shadow-[0_6px_24px_-10px_rgba(0,0,0,0.6)] hover:bg-white/[0.08] transition cursor-pointer" onclick="editInvestment(${investment.id})">
<span class="absolute inset-y-0 left-0 w-1 rounded-l-lg sm:rounded-l-xl bg-gradient-to-b ${investment.color}"></span>
<div class="pl-2 sm:pl-3 flex-1 min-w-0">
<h3 class="text-xs sm:text-sm font-medium tracking-tight truncate">${investment.title}</h3>
<div class="mt-0.5 flex items-center gap-1.5 text-[10px] sm:text-xs text-neutral-400">
<i data-lucide="${investment.icon}" class="h-3 w-3 text-[#2a7fff] flex-shrink-0"></i>
<span class="truncate">${formatAmount(investment.amount)}</span>
</div>
</div>
<div class="flex items-center gap-1 sm:gap-1.5 flex-shrink-0 ml-2">
<img alt="Assignee" class="h-5 w-5 sm:h-6 sm:w-6 rounded-full ring-1 ring-white/10 object-cover" src="https://images.unsplash.com/photo-1621619856624-42fd193a0661?w=1080&q=80" />
<div class="h-5 w-5 sm:h-6 sm:w-6 rounded-full bg-white/10 text-[10px] sm:text-[11px] text-neutral-200 grid place-items-center">+${investment.teamSize}</div>
</div>
</div>
</div>
`;
});
tasksHTML += '</div>';
container.innerHTML = tasksHTML;
if (window.lucide) lucide.createIcons();
}
function updateCurrentMarker() {
const marker = document.getElementById('current-day-marker');
const { getCurrentMarkerPosition, showCurrentMarker } = currentTimelineData;
if (showCurrentMarker) {
marker.style.left = getCurrentMarkerPosition();
marker.classList.remove('hidden');
} else {
marker.classList.add('hidden');
}
}
function getFilteredInvestments() {
const { investments } = currentTimelineData;
let filteredInvestments = investments.filter(investment => {
return activeFilters.length === 0 || activeFilters.includes(investment.type);
});
if (currentSort === 'date') {
filteredInvestments.sort((a, b) => a.startDate - b.startDate);
} else if (currentSort === 'amount') {
filteredInvestments.sort((a, b) => b.amount - a.amount);
} else if (currentSort === 'type') {
filteredInvestments.sort((a, b) => a.type.localeCompare(b.type));
}
return filteredInvestments;
}
function getColumnForDate(date) {
const { startDate, columns } = currentTimelineData;
if (currentPeriod === 'week') {
const dayOfWeek = date.getDay();
return Math.max(1, Math.min(columns, dayOfWeek + 1));
} else if (currentPeriod === 'month') {
const dayOfMonth = date.getDate();
return Math.max(1, Math.min(columns, dayOfMonth));
} else if (currentPeriod === 'quarter') {
const baseDate = new Date(2023, 9, 27);
const dayDiff = Math.floor((date - baseDate) / (1000 * 60 * 60 * 24));
return Math.max(1, Math.min(columns, dayDiff + 1));
} else if (currentPeriod === 'year') {
const month = date.getMonth();
return Math.max(1, Math.min(columns, month + 1));
}
return 1;
}
function formatAmount(amount) {
if (amount >= 1000000) {
return `$${(amount / 1000000).toFixed(1)}M`;
} else if (amount >= 1000) {
return `$${(amount / 1000).toFixed(0)}K`;
} else {
return `$${amount.toLocaleString()}`;
}
}
function updateStats() {
const { investments } = currentTimelineData;
const totalAmount = investments.reduce((sum, inv) => sum + inv.amount, 0);
document.getElementById('total-managed').textContent = `${formatAmount(totalAmount)} managed`;
document.getElementById('active-positions').textContent = `${investments.length} active positions`;
}
function closeModal() {
document.getElementById('investment-modal').classList.add('hidden');
document.getElementById('investment-form').reset();
}
function addNewInvestment() {
const title = document.getElementById('investment-title').value;
const type = document.getElementById('investment-type').value;
const startDate = new Date(document.getElementById('investment-start').value);
const endDate = new Date(document.getElementById('investment-end').value);
const amountText = document.getElementById('investment-amount').value;
if (!title || !amountText) {
alert('Please fill in all required fields');
return;
}
const amount = parseFloat(amountText.replace(/[$,]/g, ''));
if (isNaN(amount) || amount <= 0) {
alert('Please enter a valid amount');
return;
}
const newInvestment = {
id: currentTimelineData.investments.length + 1,
title,
type,
startDate,
endDate,
amount,
color: getColorForType(type),
icon: getIconForType(type),
teamSize: Math.floor(Math.random() * 4) + 1
};
currentTimelineData.investments.push(newInvestment);
investmentData[currentPeriod].push(newInvestment);
renderTimeline();
updateStats();
closeModal();
if (!activeFilters.includes(type)) {
activeFilters.push(type);
document.querySelector(`[data-filter="${type}"]`).checked = true;
updateFilterCount();
}
}
function getColorForType(type) {
const colors = {
analysis: 'from-[#2a7fff] to-[#0ea5e9]',
esg: 'from-[#22d3ee] to-[#14b8a6]',
derivatives: 'from-[#a78bfa] to-[#7c3aed]'
};
return colors[type] || 'from-[#6b7280] to-[#4b5563]';
}
function getIconForType(type) {
const icons = {
analysis: 'trending-up',
esg: 'leaf',
derivatives: 'shield'
};
return icons[type] || 'circle';
}
function editInvestment(id) {
const investment = currentTimelineData.investments.find(inv => inv.id === id);
if (investment) {
console.log('Editing investment:', investment.title);
}
}
window.addEventListener('resize', () => {
if (activeTab === 'timeline') {
renderTimeline();
}
});
updateFilterCount();
</script>
</body></html>