diff --git a/ui-v2/electron/main.ts b/ui-v2/electron/main.ts index b329fd92..bd3e79de 100644 --- a/ui-v2/electron/main.ts +++ b/ui-v2/electron/main.ts @@ -26,8 +26,12 @@ async function createWindow() { // Create the browser window with headless options when needed const mainWindow = new BrowserWindow({ + titleBarStyle: process.platform === 'darwin' ? 'hidden' : 'default', + trafficLightPosition: process.platform === 'darwin' ? { x: 16, y: 10 } : undefined, + frame: false, width: 1200, height: 800, + minWidth: 800, ...(isHeadless ? { show: false, diff --git a/ui-v2/src/App.tsx b/ui-v2/src/App.tsx index fc11393e..937b09b8 100644 --- a/ui-v2/src/App.tsx +++ b/ui-v2/src/App.tsx @@ -2,19 +2,12 @@ import React, { Suspense } from 'react'; import { Outlet } from '@tanstack/react-router'; -import GooseLogo from './components/GooseLogo'; import SuspenseLoader from './components/SuspenseLoader'; const App: React.FC = (): React.ReactElement => { return ( }> -
-
- -

Goose v2

-
- -
+
); }; diff --git a/ui-v2/src/assets/backgrounds/clock-bg.png b/ui-v2/src/assets/backgrounds/clock-bg.png new file mode 100644 index 00000000..176c01a7 Binary files /dev/null and b/ui-v2/src/assets/backgrounds/clock-bg.png differ diff --git a/ui-v2/src/assets/backgrounds/wave-bg.png b/ui-v2/src/assets/backgrounds/wave-bg.png new file mode 100644 index 00000000..176c01a7 Binary files /dev/null and b/ui-v2/src/assets/backgrounds/wave-bg.png differ diff --git a/ui-v2/src/assets/logo.svg b/ui-v2/src/assets/logo.svg new file mode 100644 index 00000000..04aa42d4 --- /dev/null +++ b/ui-v2/src/assets/logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/ui-v2/src/components/BrandCard.tsx b/ui-v2/src/components/BrandCard.tsx new file mode 100644 index 00000000..d2512043 --- /dev/null +++ b/ui-v2/src/components/BrandCard.tsx @@ -0,0 +1,139 @@ +import React from 'react'; + +interface BrandCardProps { + date?: Date; + className?: string; +} + +// Array of congratulatory messages for past days +const pastDayMessages = [ + { title: "Great work!", message: "You accomplished so much" }, + { title: "Well done!", message: "Another successful day" }, + { title: "Fantastic job!", message: "Making progress every day" }, + { title: "Nice one!", message: "Another day in the books" }, + { title: "Awesome work!", message: "Keep up the momentum" } +]; + +export default function BrandCard({ date, className = '' }: BrandCardProps) { + const isToday = date ? new Date().toDateString() === date.toDateString() : true; + + // Get a consistent message for each date + const getPastDayMessage = (date: Date) => { + // Use the date's day as an index to select a message + const index = date.getDate() % pastDayMessages.length; + return pastDayMessages[index]; + }; + + // Get message for past days + const pastMessage = date ? getPastDayMessage(date) : pastDayMessages[0]; + + return ( +
+ {/* Content */} +
+ {/* Logo */} +
+ + + + + + + + + + + + + + + +
+
+ + {/* Text content - bottom */} +
+ {isToday ? ( + <> + {/* Today's content */} +

+ Good morning +

+ +

+ You've got 3 major updates this morning +

+ + ) : ( + <> + {/* Past/Future date content */} +

+ {pastMessage?.title || 'Hello'} +

+ +

+ {pastMessage?.message || 'Great work'} +

+ + )} +
+
+ ); +} diff --git a/ui-v2/src/components/Button.test.tsx b/ui-v2/src/components/Button.test.tsx deleted file mode 100644 index ee16a9a8..00000000 --- a/ui-v2/src/components/Button.test.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { describe, it, expect, vi } from 'vitest'; - -import { Button } from './Button'; - -describe('Button', () => { - it('renders with children', () => { - render(); - expect(screen.getByText('Click me')).toBeInTheDocument(); - }); - - it('calls onClick handler when clicked', async () => { - const handleClick = vi.fn(); - render(); - - await userEvent.click(screen.getByText('Click me')); - expect(handleClick).toHaveBeenCalledTimes(1); - }); - - it('applies variant styles correctly', () => { - render(); - const button = screen.getByText('Secondary Button'); - - expect(button).toHaveStyle({ - backgroundColor: '#6c757d', - }); - }); -}); diff --git a/ui-v2/src/components/Button.tsx b/ui-v2/src/components/Button.tsx deleted file mode 100644 index 8a8bbf4a..00000000 --- a/ui-v2/src/components/Button.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; - -import { electronService } from '../services/electron'; - -interface ButtonProps { - onClick?: () => void; - children: React.ReactNode; - copyText?: string; - variant?: 'primary' | 'secondary'; - className?: string; -} - -export const Button: React.FC = ({ - onClick, - children, - copyText, - variant = 'primary', - className = '', -}) => { - const handleClick = async () => { - if (copyText) { - try { - console.log('Attempting to copy text:', copyText); - await electronService.copyToClipboard(copyText); - console.log('Text copied successfully'); - } catch (error) { - console.error('Failed to copy:', error); - } - } - - if (onClick) { - onClick(); - } - }; - - const getVariantStyles = () => { - switch (variant) { - case 'secondary': - return { - backgroundColor: '#6c757d', - color: 'white', - }; - case 'primary': - default: - return { - backgroundColor: '#4CAF50', - color: 'white', - }; - } - }; - - return ( - - ); -}; diff --git a/ui-v2/src/components/ChartTile.tsx b/ui-v2/src/components/ChartTile.tsx new file mode 100644 index 00000000..74dbece4 --- /dev/null +++ b/ui-v2/src/components/ChartTile.tsx @@ -0,0 +1,152 @@ +import React from 'react'; +import { useTimelineStyles } from '../hooks/useTimelineStyles'; + +interface ChartTileProps { + title: string; + value: string; + trend?: string; + data: number[]; + icon: React.ReactNode; + variant?: 'line' | 'bar'; + date?: Date; +} + +export default function ChartTile({ + title, + value, + trend, + data, + icon, + variant = 'line', + date +}: ChartTileProps) { + const { contentCardStyle } = useTimelineStyles(date); + + // Convert data points to SVG coordinates + const createSmoothPath = () => { + const points = data.map((value, index) => { + const x = (index / (data.length - 1)) * 100; + const y = 100 - ((value - Math.min(...data)) / (Math.max(...data) - Math.min(...data))) * 100; + return [x, y]; + }); + + let path = `M ${points[0][0]},${points[0][1]}`; + for (let i = 0; i < points.length - 1; i++) { + const current = points[i]; + const next = points[i + 1]; + const controlPoint1X = current[0] + (next[0] - current[0]) / 3; + const controlPoint2X = current[0] + 2 * (next[0] - current[0]) / 3; + path += ` C ${controlPoint1X},${current[1]} ${controlPoint2X},${next[1]} ${next[0]},${next[1]}`; + } + return path; + }; + + // Create bar chart elements + const createBars = () => { + const maxValue = Math.max(...data.map(Math.abs)); + const barWidth = 8; + const spacing = (100 - (data.length * barWidth)) / (data.length - 1); + + return data.map((value, index) => { + const x = index * (barWidth + spacing); + const height = Math.abs(value) / maxValue * 50; + const y = value > 0 ? 50 - height : 50; + + return { + x, + y, + height, + isPositive: value > 0 + }; + }); + }; + + return ( +
+ {/* Header section with icon */} +
+
+ {icon} +
+ +
+
{title}
+
+ {value} + {trend && {trend}} +
+
+
+ + {/* Chart Container */} +
+ + {variant === 'line' ? ( + <> + + + + + + + + + + {/* Data points */} + {data.map((value, index) => { + const x = (index / (data.length - 1)) * 100; + const y = 100 - ((value - Math.min(...data)) / (Math.max(...data) - Math.min(...data))) * 100; + return ( + + ); + })} + + ) : ( + <> + {createBars().map((bar, index) => ( + + ))} + + )} + +
+
+ ); +} \ No newline at end of file diff --git a/ui-v2/src/components/ClockTile.tsx b/ui-v2/src/components/ClockTile.tsx new file mode 100644 index 00000000..1343e995 --- /dev/null +++ b/ui-v2/src/components/ClockTile.tsx @@ -0,0 +1,80 @@ + +import React, { useState, useEffect } from 'react'; +import { useTimelineStyles } from '../hooks/useTimelineStyles'; +import waveBg from '../assets/backgrounds/wave-bg.png'; + +interface ClockCardProps { + date?: Date; +} + +export default function ClockTile({ date }: ClockCardProps) { + const { contentCardStyle, isPastDate } = useTimelineStyles(date); + const [currentTime, setCurrentTime] = useState(new Date()); + + // Don't render for past dates + if (isPastDate) { + return null; + } + + // Update time every second for current day + useEffect(() => { + const timer = setInterval(() => { + setCurrentTime(new Date()); + }, 1000); + + return () => clearInterval(timer); + }, []); + + // Format hours (12-hour format) + const hours = currentTime.getHours() % 12 || 12; + const minutes = currentTime.getMinutes().toString().padStart(2, '0'); + const period = currentTime.getHours() >= 12 ? 'PM' : 'AM'; + + // Format day name + const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + const dayName = dayNames[currentTime.getDay()]; + + return ( +
+ {/* Background Image with Gradient Overlay */} +
+ + {/* Gradient Overlay */} +
+ + {/* Time Display */} +
+
+ + {hours}:{minutes} + + + {period} + +
+ + {dayName} + +
+
+ ); +} \ No newline at end of file diff --git a/ui-v2/src/components/GooseLogo.tsx b/ui-v2/src/components/GooseLogo.tsx deleted file mode 100644 index bd1f026e..00000000 --- a/ui-v2/src/components/GooseLogo.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { FC } from 'react'; - -import { Goose, Rain } from './icons/Goose'; - -interface GooseLogoProps { - className?: string; - size?: 'default' | 'small'; - hover?: boolean; -} - -const GooseLogo: FC = ({ className = '', size = 'default', hover = true }) => { - const sizes = { - default: { - frame: 'w-16 h-16', - rain: 'w-[275px] h-[275px]', - goose: 'w-16 h-16', - }, - small: { - frame: 'w-8 h-8', - rain: 'w-[150px] h-[150px]', - goose: 'w-8 h-8', - }, - }; - return ( -
- - -
- ); -}; - -export default GooseLogo; diff --git a/ui-v2/src/components/HighlightTile.tsx b/ui-v2/src/components/HighlightTile.tsx new file mode 100644 index 00000000..cb44f526 --- /dev/null +++ b/ui-v2/src/components/HighlightTile.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { useTimelineStyles } from '../hooks/useTimelineStyles'; + +interface HighlightTileProps { + title: string; + value: string; + icon: React.ReactNode; + subtitle?: string; + date?: Date; + accentColor?: string; +} + +export default function HighlightTile({ + title, + value, + icon, + subtitle, + date, + accentColor = '#00CAF7' +}: HighlightTileProps) { + const { contentCardStyle } = useTimelineStyles(date); + + return ( +
+ {/* Background accent */} +
+ + {/* Content */} +
+
+ {icon} +
+ +
+
{title}
+
+ {value} +
+ {subtitle && ( +
+ {subtitle} +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/ui-v2/src/components/ListTile.tsx b/ui-v2/src/components/ListTile.tsx new file mode 100644 index 00000000..8c41cccf --- /dev/null +++ b/ui-v2/src/components/ListTile.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { useTimelineStyles } from '../hooks/useTimelineStyles'; + +interface ListItem { + text: string; + value?: string; + color?: string; +} + +interface ListTileProps { + title: string; + icon: React.ReactNode; + items: ListItem[]; + date?: Date; +} + +export default function ListTile({ + title, + icon, + items, + date +}: ListTileProps) { + const { contentCardStyle } = useTimelineStyles(date); + + return ( +
+ {/* Header */} +
+
+ {icon} +
+
+ {title} +
+
+ + {/* List */} +
+
+ {items.map((item, index) => ( +
+
+
+ + {item.text} + +
+ {item.value && ( + + {item.value} + + )} +
+ ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/ui-v2/src/components/PieChartTile.tsx b/ui-v2/src/components/PieChartTile.tsx new file mode 100644 index 00000000..62585218 --- /dev/null +++ b/ui-v2/src/components/PieChartTile.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { useTimelineStyles } from '../hooks/useTimelineStyles'; + +interface PieChartSegment { + value: number; + color: string; + label: string; +} + +interface PieChartTileProps { + title: string; + icon: React.ReactNode; + segments: PieChartSegment[]; + date?: Date; +} + +export default function PieChartTile({ + title, + icon, + segments, + date +}: PieChartTileProps) { + const { contentCardStyle } = useTimelineStyles(date); + + const total = segments.reduce((sum, segment) => sum + segment.value, 0); + let currentAngle = 0; + + const createPieSegments = () => { + return segments.map((segment, index) => { + const startAngle = currentAngle; + const percentage = segment.value / total; + const angle = percentage * 360; + currentAngle += angle; + + const startX = Math.cos((startAngle - 90) * Math.PI / 180) * 32 + 50; + const startY = Math.sin((startAngle - 90) * Math.PI / 180) * 32 + 50; + const endX = Math.cos((currentAngle - 90) * Math.PI / 180) * 32 + 50; + const endY = Math.sin((currentAngle - 90) * Math.PI / 180) * 32 + 50; + + const largeArcFlag = angle > 180 ? 1 : 0; + + const d = [ + `M 50 50`, + `L ${startX} ${startY}`, + `A 32 32 0 ${largeArcFlag} 1 ${endX} ${endY}`, + 'Z' + ].join(' '); + + return { + path: d, + color: segment.color, + label: segment.label, + percentage: (percentage * 100).toFixed(1) + }; + }); + }; + + const pieSegments = createPieSegments(); + + return ( +
+ {/* Header */} +
+
+ {icon} +
+
+ {title} +
+
+ + {/* Pie Chart */} +
+
+ + {pieSegments.map((segment, index) => ( + + ))} + +
+ + {/* Legend */} +
+ {pieSegments.map((segment, index) => ( +
+
+
+ + {segment.label} + +
+ + {segment.percentage}% + +
+ ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/ui-v2/src/components/Timeline.tsx b/ui-v2/src/components/Timeline.tsx new file mode 100644 index 00000000..5cd6d83b --- /dev/null +++ b/ui-v2/src/components/Timeline.tsx @@ -0,0 +1,413 @@ +import React, { useRef, useMemo, useEffect } from 'react'; +import ChartTile from './ChartTile'; +import HighlightTile from './HighlightTile'; +import PieChartTile from './PieChartTile'; +import ListTile from './ListTile'; +import ClockTile from './ClockTile'; +import TimelineDots from './TimelineDots'; +import { ChartLineIcon, ChartBarIcon, PieChartIcon, ListIcon, StarIcon, TrendingUpIcon } from './icons'; + +const generateRandomData = (length: number) => Array.from({ length }, () => Math.floor(Math.random() * 100)); + +const generateTileData = (date: Date) => { + const isToday = new Date().toDateString() === date.toDateString(); + + return { + left: [ + // Performance metrics + { + type: 'chart' as const, + props: { + title: 'Daily Activity', + value: '487', + trend: '↑ 12%', + data: generateRandomData(7), + icon: , + variant: 'line' as const, + date + } + }, + { + type: 'highlight' as const, + props: { + title: 'Achievement', + value: isToday ? 'New Record!' : 'Great Work', + icon: , + subtitle: isToday ? 'Personal best today' : 'Keep it up', + date, + accentColor: '#FFB800' + } + }, + { + type: 'pie' as const, + props: { + title: 'Task Distribution', + icon: , + segments: [ + { value: 45, color: '#00CAF7', label: 'Completed' }, + { value: 35, color: '#FFB800', label: 'In Progress' }, + { value: 20, color: '#FF4444', label: 'Pending' } + ], + date + } + }, + // Additional metrics + { + type: 'chart' as const, + props: { + title: 'Response Time', + value: '245ms', + trend: '↓ 18%', + data: generateRandomData(7), + icon: , + variant: 'bar' as const, + date + } + }, + { + type: 'highlight' as const, + props: { + title: 'User Satisfaction', + value: '98%', + icon: , + subtitle: 'Based on feedback', + date, + accentColor: '#4CAF50' + } + }, + { + type: 'list' as const, + props: { + title: 'Top Priorities', + icon: , + items: [ + { text: 'Project Alpha', value: '87%', color: '#00CAF7' }, + { text: 'Team Meeting', value: '2:30 PM' }, + { text: 'Review Code', value: '13', color: '#FFB800' }, + { text: 'Deploy Update', value: 'Done', color: '#4CAF50' } + ], + date + } + }, + // System metrics + { + type: 'chart' as const, + props: { + title: 'System Load', + value: '42%', + trend: '↑ 5%', + data: generateRandomData(7), + icon: , + variant: 'line' as const, + date + } + }, + { + type: 'pie' as const, + props: { + title: 'Storage Usage', + icon: , + segments: [ + { value: 60, color: '#4CAF50', label: 'Free' }, + { value: 25, color: '#FFB800', label: 'Used' }, + { value: 15, color: '#FF4444', label: 'System' } + ], + date + } + } + ], + right: [ + // Performance metrics + { + type: 'chart' as const, + props: { + title: 'Performance', + value: '92%', + trend: '↑ 8%', + data: generateRandomData(7), + icon: , + variant: 'bar' as const, + date + } + }, + // Clock tile + { + type: 'clock' as const, + props: { + title: 'Current Time', + date + } + }, + { + type: 'highlight' as const, + props: { + title: 'Efficiency', + value: '+28%', + icon: , + subtitle: 'Above target', + date, + accentColor: '#4CAF50' + } + }, + { + type: 'pie' as const, + props: { + title: 'Resource Usage', + icon: , + segments: [ + { value: 55, color: '#4CAF50', label: 'Available' }, + { value: 30, color: '#FFB800', label: 'In Use' }, + { value: 15, color: '#FF4444', label: 'Reserved' } + ], + date + } + }, + // Updates and notifications + { + type: 'list' as const, + props: { + title: 'Recent Updates', + icon: , + items: [ + { text: 'System Update', value: 'Complete', color: '#4CAF50' }, + { text: 'New Features', value: '3', color: '#00CAF7' }, + { text: 'Bug Fixes', value: '7', color: '#FFB800' }, + { text: 'Performance', value: '+15%', color: '#4CAF50' } + ], + date + } + }, + // Additional metrics + { + type: 'chart' as const, + props: { + title: 'User Activity', + value: '1,247', + trend: '↑ 23%', + data: generateRandomData(7), + icon: , + variant: 'line' as const, + date + } + }, + { + type: 'highlight' as const, + props: { + title: 'New Users', + value: '+156', + icon: , + subtitle: 'Last 24 hours', + date, + accentColor: '#00CAF7' + } + }, + // System health + { + type: 'pie' as const, + props: { + title: 'API Health', + icon: , + segments: [ + { value: 75, color: '#4CAF50', label: 'Healthy' }, + { value: 20, color: '#FFB800', label: 'Warning' }, + { value: 5, color: '#FF4444', label: 'Critical' } + ], + date + } + }, + { + type: 'list' as const, + props: { + title: 'System Status', + icon: , + items: [ + { text: 'Main API', value: 'Online', color: '#4CAF50' }, + { text: 'Database', value: '98%', color: '#00CAF7' }, + { text: 'Cache', value: 'Synced', color: '#4CAF50' }, + { text: 'CDN', value: 'Active', color: '#4CAF50' } + ], + date + } + } + ] + }; +}; + +export default function Timeline() { + const containerRef = useRef(null); + const sectionRefs = useRef<(HTMLDivElement | null)[]>([]); + + const sections = useMemo(() => { + const result = []; + const today = new Date(); + + for (let i = 0; i <= 29; i++) { + const date = new Date(today); + date.setDate(today.getDate() - i); + + const tileData = generateTileData(date); + + result.push({ + date, + isToday: i === 0, + leftTiles: tileData.left, + rightTiles: tileData.right + }); + } + + return result; + }, []); + + // Function to center the timeline in a section + const centerTimeline = (sectionElement: HTMLDivElement) => { + if (!sectionElement) return; + + requestAnimationFrame(() => { + const totalWidth = sectionElement.scrollWidth; + const viewportWidth = sectionElement.clientWidth; + const scrollToX = Math.max(0, (totalWidth - viewportWidth) / 2); + + sectionElement.scrollTo({ + left: scrollToX, + behavior: 'smooth' + }); + }); + }; + + useEffect(() => { + // Create the intersection observer + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const section = entry.target as HTMLDivElement; + centerTimeline(section); + } + }); + }, + { + threshold: 0.5, + rootMargin: '0px' + } + ); + + // Add resize handler + const handleResize = () => { + // Find the currently visible section + const visibleSection = sectionRefs.current.find(section => { + if (!section) return false; + const rect = section.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + // Check if the section is mostly visible in the viewport + return rect.top >= -viewportHeight / 2 && rect.bottom <= viewportHeight * 1.5; + }); + + if (visibleSection) { + centerTimeline(visibleSection); + } + }; + + // Add resize event listener + window.addEventListener('resize', handleResize); + + // Observe all sections + sectionRefs.current.forEach((section) => { + if (section) { + observer.observe(section); + centerTimeline(section); + } + }); + + // Cleanup function + return () => { + window.removeEventListener('resize', handleResize); + sectionRefs.current.forEach((section) => { + if (section) { + observer.unobserve(section); + } + }); + }; + }, []); + + const renderTile = (tile: any, index: number) => { + switch (tile.type) { + case 'chart': + return ; + case 'highlight': + return ; + case 'pie': + return ; + case 'list': + return ; + case 'clock': + return ; + default: + return null; + } + }; + + return ( +
+ {sections.map((section, index) => ( +
sectionRefs.current[index] = el} + className="h-screen relative snap-center snap-always overflow-x-scroll snap-x snap-mandatory scrollbar-hide" + > +
+ {/* Main flex container */} +
+ + {/* Left Grid */} +
+
+ {section.leftTiles.map((tile, i) => ( +
+ {renderTile(tile, i)} +
+ ))} +
+
+ + {/* Center Timeline */} +
+ {/* Upper Timeline Dots */} + + + {/* Date Display */} +
+
+ {section.date.toLocaleString('default', { month: 'short' })} +
+
+ {section.date.getDate()} +
+
+ {section.date.toLocaleString('default', { weekday: 'long' })} +
+
+ + {/* Lower Timeline Dots */} + +
+ + {/* Right Grid */} +
+
+ {section.rightTiles.map((tile, i) => ( +
+ {renderTile(tile, i)} +
+ ))} +
+
+
+
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/ui-v2/src/components/TimelineContext.tsx b/ui-v2/src/components/TimelineContext.tsx new file mode 100644 index 00000000..65275961 --- /dev/null +++ b/ui-v2/src/components/TimelineContext.tsx @@ -0,0 +1,31 @@ +import React, { createContext, useContext, useState, useCallback } from 'react'; + +interface TimelineContextType { + currentDate: Date; + setCurrentDate: (date: Date) => void; + isCurrentDate: (date: Date) => boolean; +} + +const TimelineContext = createContext(undefined); + +export function TimelineProvider({ children }: { children: React.ReactNode }) { + const [currentDate, setCurrentDate] = useState(new Date()); + + const isCurrentDate = useCallback((date: Date) => { + return date.toDateString() === new Date().toDateString(); + }, []); + + return ( + + {children} + + ); +} + +export function useTimeline() { + const context = useContext(TimelineContext); + if (context === undefined) { + throw new Error('useTimeline must be used within a TimelineProvider'); + } + return context; +} diff --git a/ui-v2/src/components/TimelineDots.tsx b/ui-v2/src/components/TimelineDots.tsx new file mode 100644 index 00000000..ef152a76 --- /dev/null +++ b/ui-v2/src/components/TimelineDots.tsx @@ -0,0 +1,96 @@ +import React, { useMemo } from 'react'; + +interface TimelineDotsProps { + height: number | string; + isUpper?: boolean; + isCurrentDay?: boolean; +} + +interface Dot { + top: string; + size: number; + opacity: number; +} + +export default function TimelineDots({ height, isUpper = false, isCurrentDay = false }: TimelineDotsProps) { + // Generate random dots with clusters + const dots = useMemo(() => { + const generateDots = () => { + const dots: Dot[] = []; + const numDots = Math.floor(Math.random() * 8) + 8; // 8-15 dots + + // Create 2-3 cluster points + const clusterPoints = Array.from({ length: Math.floor(Math.random() * 2) + 2 }, + () => Math.random() * 100); + + for (let i = 0; i < numDots; i++) { + // Decide if this dot should be part of a cluster + const isCluster = Math.random() < 0.7; // 70% chance of being in a cluster + + let top; + if (isCluster) { + // Pick a random cluster point and add some variation + const clusterPoint = clusterPoints[Math.floor(Math.random() * clusterPoints.length)]; + top = clusterPoint + (Math.random() - 0.5) * 15; // ±7.5% variation + } else { + top = Math.random() * 100; + } + + // Ensure dot is within bounds + top = Math.max(5, Math.min(95, top)); + + dots.push({ + top: `${top}%`, + size: Math.random() * 2 + 2, // 2-4px + opacity: Math.random() * 0.5 + 0.2, // 0.2-0.7 opacity + }); + } + return dots; + }; + + return generateDots(); + }, []); // Empty dependency array means this only runs once + + return ( +
+ {/* Main line */} +
+ {/* Top dot for current day */} + {isUpper && isCurrentDay && ( +
+ )} + + {/* Random dots */} + {dots.map((dot, index) => ( +
+ ))} +
+
+ ); +}; diff --git a/ui-v2/src/components/ValueCard.tsx b/ui-v2/src/components/ValueCard.tsx new file mode 100644 index 00000000..c85c3e18 --- /dev/null +++ b/ui-v2/src/components/ValueCard.tsx @@ -0,0 +1,130 @@ +import React from 'react'; + +interface BrandCardProps { + date?: Date; + className?: string; +} + +export default function BrandCard({ }: BrandCardProps) { + const isToday = date ? new Date().toDateString() === date.toDateString() : true; + + // Get a consistent message for each date + const getPastDayMessage = (date: Date) => { + // Use the date's day as an index to select a message + const index = date.getDate() % pastDayMessages.length; + return pastDayMessages[index]; + }; + + // Get message for past days + const pastMessage = date ? getPastDayMessage(date) : pastDayMessages[0]; + + return ( +
+ {/* Content */} +
+ {/* Logo */} +
+ + + + + + + + + + + + + + + +
+
+ + {/* Text content - bottom */} +
+ {isToday ? ( + <> + {/* Today's content */} +

+ Good morning +

+ +

+ You've got 3 major updates this morning +

+ + ) : ( + <> + {/* Past/Future date content */} +

+ {pastMessage?.title || 'Hello'} +

+ +

+ {pastMessage?.message || 'Great work'} +

+ + )} +
+
+ ); +} diff --git a/ui-v2/src/components/icons.tsx b/ui-v2/src/components/icons.tsx new file mode 100644 index 00000000..3dd6fb0f --- /dev/null +++ b/ui-v2/src/components/icons.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +export const ChartLineIcon = () => ( + + + +); + +export const ChartBarIcon = () => ( + + + +); + +export const PieChartIcon = () => ( + + + +); + +export const ListIcon = () => ( + + + +); + +export const StarIcon = () => ( + + + +); + +export const TrendingUpIcon = () => ( + + + +); \ No newline at end of file diff --git a/ui-v2/src/components/icons/Goose.tsx b/ui-v2/src/components/icons/Goose.tsx deleted file mode 100644 index 49d6178c..00000000 --- a/ui-v2/src/components/icons/Goose.tsx +++ /dev/null @@ -1,392 +0,0 @@ -import { FC } from 'react'; - -interface IconProps { - className?: string; -} - -export const Goose: FC = ({ className = '' }) => { - return ( - - - - - - - - - - - ); -}; - -export const Rain: FC = ({ className = '' }) => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/ui-v2/src/hooks/useTimelineStyles.ts b/ui-v2/src/hooks/useTimelineStyles.ts new file mode 100644 index 00000000..60a90eaa --- /dev/null +++ b/ui-v2/src/hooks/useTimelineStyles.ts @@ -0,0 +1,35 @@ +import { useTimeline } from '../components/TimelineContext'; + +interface TimelineStyles { + isPastDate: boolean; + greetingCardStyle: { + background: string; + text: string; + }; + contentCardStyle: string; +} + +export function useTimelineStyles(date?: Date): TimelineStyles { + const { isCurrentDate } = useTimeline(); + const isPastDate = date && date < new Date() && !isCurrentDate(date); + + // Content cards match the Tasks Completed tile styling + const contentCardStyle = 'bg-white dark:bg-[#121212] shadow-[0_0_13.7px_rgba(0,0,0,0.04)] dark:shadow-[0_0_24px_rgba(255,255,255,0.02)]'; + + // Greeting card styles based on date + const greetingCardStyle = !isPastDate + ? { + background: 'bg-textStandard', // Black background + text: 'text-white' // White text + } + : { + background: 'bg-gray-100', // Light grey background + text: 'text-gray-600' // Darker grey text + }; + + return { + isPastDate, + greetingCardStyle, + contentCardStyle + }; +} diff --git a/ui-v2/src/routeTree.ts b/ui-v2/src/routeTree.ts index dd3ba914..d4e91817 100644 --- a/ui-v2/src/routeTree.ts +++ b/ui-v2/src/routeTree.ts @@ -1,8 +1,8 @@ import { createRouter } from '@tanstack/react-router'; -import { rootRoute, indexRoute, aboutRoute } from './routes'; +import { rootRoute, timelineRoute } from './routes'; -const routeTree = rootRoute.addChildren([indexRoute, aboutRoute]); +const routeTree = rootRoute.addChildren([timelineRoute]); export const router = createRouter({ routeTree }); diff --git a/ui-v2/src/routes/__root.tsx b/ui-v2/src/routes/__root.tsx index 9b5d54b8..ef7d5032 100644 --- a/ui-v2/src/routes/__root.tsx +++ b/ui-v2/src/routes/__root.tsx @@ -6,11 +6,8 @@ export const Route = createRootRoute({ <>
- Home + Timeline {' '} - - About -

diff --git a/ui-v2/src/routes/index.tsx b/ui-v2/src/routes/index.tsx index 6308b9ab..e41f49cd 100644 --- a/ui-v2/src/routes/index.tsx +++ b/ui-v2/src/routes/index.tsx @@ -1,28 +1,21 @@ import { createRootRoute, createRoute } from '@tanstack/react-router'; import App from '../App'; +import Timeline from '../components/Timeline'; +import { TimelineProvider } from '../components/TimelineContext'; export const rootRoute = createRootRoute({ component: App, }); -export const indexRoute = createRoute({ +export const timelineRoute = createRoute({ getParentRoute: () => rootRoute, path: '/', component: () => ( -
-

Welcome to Goose v2

-
- ), -}); - -export const aboutRoute = createRoute({ - getParentRoute: () => rootRoute, - path: '/about', - component: () => ( -
-

About Goose v2

-

An AI assistant for developers

-
+ +
+ +
+
), }); diff --git a/ui-v2/tailwind.config.js b/ui-v2/tailwind.config.js index e0a8acef..ff8e1f90 100644 --- a/ui-v2/tailwind.config.js +++ b/ui-v2/tailwind.config.js @@ -1,6 +1,7 @@ /** @type {import('tailwindcss').Config} */ export default { content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + darkMode: 'media', // Enable dark mode and use the system preference theme: { extend: { colors: { @@ -10,4 +11,4 @@ export default { }, }, plugins: [], -}; +}; \ No newline at end of file