From b5884a448b1209047b894dbded6a03eb89cde31d Mon Sep 17 00:00:00 2001 From: Zane <75694352+zanesq@users.noreply.github.com> Date: Wed, 28 May 2025 14:33:02 -0700 Subject: [PATCH] ui-v2 cleanup (#2701) --- .husky/pre-commit | 6 + ui-v2/.stylelintrc.json | 4 +- ui-v2/electron/main.ts | 2 +- ui-v2/eslint.config.cjs | 21 +- ui-v2/lib/utils.ts | 8 +- ui-v2/package-lock.json | 106 ++++- ui-v2/package.json | 3 +- ui-v2/src/App.tsx | 8 - ui-v2/src/components/BrandCard.tsx | 44 +- ui-v2/src/components/DarkModeToggle.tsx | 11 +- ui-v2/src/components/DateDisplay.tsx | 26 +- ui-v2/src/components/GooseLogo.tsx | 42 ++ ui-v2/src/components/Home.tsx | 12 + ui-v2/src/components/Timeline.tsx | 104 +++-- ui-v2/src/components/TimelineContext.tsx | 8 +- ui-v2/src/components/TimelineDots.tsx | 46 +- ui-v2/src/components/ValueCard.tsx | 44 +- ui-v2/src/components/chat/ChatDock.tsx | 9 +- ui-v2/src/components/chat/ChatIcons.tsx | 62 ++- ui-v2/src/components/chat/ChatInput.tsx | 36 +- ui-v2/src/components/chat/FloatingChat.tsx | 25 +- .../components/filters/FloatingFilters.tsx | 26 +- ui-v2/src/components/icons.tsx | 64 ++- ui-v2/src/components/icons/Goose.tsx | 396 ++++++++++++++++++ ui-v2/src/components/tiles/ChartTile.tsx | 74 ++-- ui-v2/src/components/tiles/ClockTile.tsx | 48 +-- ui-v2/src/components/tiles/HighlightTile.tsx | 33 +- ui-v2/src/components/tiles/ListTile.tsx | 35 +- ui-v2/src/components/tiles/PieChartTile.tsx | 93 ++-- ui-v2/src/components/ui/chart.tsx | 130 +++--- ui-v2/src/components/ui/tooltip.tsx | 19 +- ui-v2/src/contexts/TimelineContext.tsx | 13 +- ui-v2/src/hooks/useTimelineStyles.ts | 15 +- ui-v2/src/layout/MainLayout.tsx | 124 +----- ui-v2/src/lib/utils.ts | 14 +- ui-v2/src/routeTree.gen.ts | 44 +- ui-v2/src/routeTree.ts | 4 +- ui-v2/src/routes/__root.tsx | 14 +- ui-v2/src/routes/index.tsx | 22 +- ui-v2/src/styles/main.css | 73 ++-- ui-v2/src/test/types.d.ts | 32 +- ui-v2/tsconfig.electron.json | 1 + 42 files changed, 1201 insertions(+), 700 deletions(-) delete mode 100644 ui-v2/src/App.tsx create mode 100644 ui-v2/src/components/GooseLogo.tsx create mode 100644 ui-v2/src/components/Home.tsx create mode 100644 ui-v2/src/components/icons/Goose.tsx diff --git a/.husky/pre-commit b/.husky/pre-commit index 5e42aa6b..f2796dca 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -5,3 +5,9 @@ if git diff --cached --name-only | grep -q "^ui/desktop/"; then . "$(dirname -- "$0")/_/husky.sh" cd ui/desktop && npx lint-staged fi + +# Only auto-format ui-v2 TS code if relevant files are modified +if git diff --cached --name-only | grep -q "^ui-v2/"; then + . "$(dirname -- "$0")/_/husky.sh" + cd ui-v2 && npx lint-staged +fi diff --git a/ui-v2/.stylelintrc.json b/ui-v2/.stylelintrc.json index 8ab0fc41..90d8df78 100644 --- a/ui-v2/.stylelintrc.json +++ b/ui-v2/.stylelintrc.json @@ -4,9 +4,11 @@ "at-rule-no-unknown": [ true, { - "ignoreAtRules": ["tailwind", "apply", "variants", "responsive", "screen"] + "ignoreAtRules": ["tailwind", "apply", "variants", "responsive", "screen", "theme", "custom-variant"] } ], + "at-rule-no-deprecated": null, + "custom-property-pattern": null, "no-descending-specificity": null } } diff --git a/ui-v2/electron/main.ts b/ui-v2/electron/main.ts index ef0890e4..d9a1b963 100644 --- a/ui-v2/electron/main.ts +++ b/ui-v2/electron/main.ts @@ -27,7 +27,7 @@ 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, + ...(process.platform === 'darwin' ? { trafficLightPosition: { x: 16, y: 10 } } : {}), frame: false, width: 1200, height: 800, diff --git a/ui-v2/eslint.config.cjs b/ui-v2/eslint.config.cjs index 4bb86216..56456936 100644 --- a/ui-v2/eslint.config.cjs +++ b/ui-v2/eslint.config.cjs @@ -89,8 +89,18 @@ module.exports = [ navigator: 'readonly', console: 'readonly', setTimeout: 'readonly', - Blob: 'readonly', + setInterval: 'readonly', + clearTimeout: 'readonly', + clearInterval: 'readonly', + requestAnimationFrame: 'readonly', + localStorage: 'readonly', + HTMLDivElement: 'readonly', + HTMLTextAreaElement: 'readonly', + HTMLFormElement: 'readonly', HTMLInputElement: 'readonly', + MutationObserver: 'readonly', + IntersectionObserver: 'readonly', + Blob: 'readonly', SVGSVGElement: 'readonly', }, }, @@ -106,6 +116,15 @@ module.exports = [ }, }, }, + // UI components (shadcn/ui) - more relaxed rules + { + files: ['src/components/ui/**/*.{ts,tsx}'], + rules: { + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + 'react/prop-types': 'off', + }, + }, // Test configuration { files: ['**/*.test.{ts,tsx}', 'src/test/**/*.{ts,tsx}'], diff --git a/ui-v2/lib/utils.ts b/ui-v2/lib/utils.ts index bd0c391d..a7c26636 100644 --- a/ui-v2/lib/utils.ts +++ b/ui-v2/lib/utils.ts @@ -1,6 +1,6 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) +export function cn(...inputs: ClassValue[]): string { + return twMerge(clsx(inputs)); } diff --git a/ui-v2/package-lock.json b/ui-v2/package-lock.json index 7c776f38..005112e7 100644 --- a/ui-v2/package-lock.json +++ b/ui-v2/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@radix-ui/react-tooltip": "^1.2.7", "@tanstack/react-router": "^1.120.5", + "@tanstack/react-router-devtools": "^1.120.11", "@tanstack/router": "^0.0.1-beta.53", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -3437,14 +3438,14 @@ } }, "node_modules/@tanstack/react-router": { - "version": "1.120.5", - "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.120.5.tgz", - "integrity": "sha512-A+YRftGwAeFBxa8DF5ujNYqkSEbjCa1KjxDNYr+jWj16jjTxrz/XqgOJCv5ZfbAqqqOa3yLYoQbWa7OGz5jHuA==", + "version": "1.120.11", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.120.11.tgz", + "integrity": "sha512-VpG8gT+kibsdF9yQIOMfnCGe1pmUlrAG/fOoTm0gru1OEkJ2Tzc80codqiocRHQ9ULmlB4H/Zx56EZyQyF3ELw==", "license": "MIT", "dependencies": { "@tanstack/history": "1.115.0", "@tanstack/react-store": "^0.7.0", - "@tanstack/router-core": "1.120.5", + "@tanstack/router-core": "1.120.10", "jsesc": "^3.1.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" @@ -3461,6 +3462,28 @@ "react-dom": ">=18.0.0 || >=19.0.0" } }, + "node_modules/@tanstack/react-router-devtools": { + "version": "1.120.11", + "resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.120.11.tgz", + "integrity": "sha512-bk34Kn7SubkUq3TbVN6wfALvOZ63ou/dzPqhijZAwHKXpatE90BwB/Y8mLhcoH+64iXtpf/ZP2lqqsrxLXz0pw==", + "license": "MIT", + "dependencies": { + "@tanstack/router-devtools-core": "^1.120.10", + "solid-js": "^1.9.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-router": "^1.120.11", + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + }, "node_modules/@tanstack/react-store": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.0.tgz", @@ -3498,9 +3521,9 @@ } }, "node_modules/@tanstack/router-core": { - "version": "1.120.5", - "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.120.5.tgz", - "integrity": "sha512-IXLNv3j7rpTL/YNCWHijZgrnxFuvD4Nz/nUiGSak4x5BKzlnuZEso81xFcIuczVrEW72NxZv8IfzpR5M5Tuc0A==", + "version": "1.120.10", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.120.10.tgz", + "integrity": "sha512-AmEJAYt+6w/790zTnfddVhnK1QJCnd96H4xg1aD65Oohc8+OTQBxgWky/wzqwhHRdkdsBgRT7iWac9x5Y8UrQA==", "license": "MIT", "dependencies": { "@tanstack/history": "1.115.0", @@ -3515,6 +3538,34 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/router-devtools-core": { + "version": "1.120.10", + "resolved": "https://registry.npmjs.org/@tanstack/router-devtools-core/-/router-devtools-core-1.120.10.tgz", + "integrity": "sha512-fysPrKH7dL/G/guHm0HN+ceFEBZnbKaU9R8KZHo/Qzue7WxQV+g4or2EWnbBJ8/aF+C/WYgxR1ATFqfZEjHSfg==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/router-core": "^1.120.10", + "csstype": "^3.0.10", + "solid-js": ">=1.9.5", + "tiny-invariant": "^1.3.3" + }, + "peerDependenciesMeta": { + "csstype": { + "optional": true + } + } + }, "node_modules/@tanstack/router/node_modules/@tanstack/store": { "version": "0.0.1-beta.52", "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.0.1-beta.52.tgz", @@ -8712,6 +8763,15 @@ "dev": true, "license": "MIT" }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -13234,6 +13294,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/seroval": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", + "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.2.tgz", + "integrity": "sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -13541,6 +13622,17 @@ "node": ">= 6.0.0" } }, + "node_modules/solid-js": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.7.tgz", + "integrity": "sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.0", + "seroval": "~1.3.0", + "seroval-plugins": "~1.3.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/ui-v2/package.json b/ui-v2/package.json index 25fc31d2..3a721621 100644 --- a/ui-v2/package.json +++ b/ui-v2/package.json @@ -29,11 +29,12 @@ "prettier:fix": "prettier --write \"src/**/*.{ts,tsx,js,jsx,css}\" \"electron/**/*.{ts,tsx,js,jsx,css}\"", "format": "npm run prettier:fix && npm run lint:style:fix", "check-all": "npm run typecheck && npm run lint && npm run prettier", - "prepare": "cd ../.. && husky install" + "prepare": "cd .. && npx husky" }, "dependencies": { "@radix-ui/react-tooltip": "^1.2.7", "@tanstack/react-router": "^1.120.5", + "@tanstack/react-router-devtools": "^1.120.11", "@tanstack/router": "^0.0.1-beta.53", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/ui-v2/src/App.tsx b/ui-v2/src/App.tsx deleted file mode 100644 index b8d9fe6a..00000000 --- a/ui-v2/src/App.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import { MainLayout } from './layout/MainLayout'; - -const App: React.FC = (): React.ReactElement => { - return ; -}; - -export default App; diff --git a/ui-v2/src/components/BrandCard.tsx b/ui-v2/src/components/BrandCard.tsx index d2512043..a01a5cec 100644 --- a/ui-v2/src/components/BrandCard.tsx +++ b/ui-v2/src/components/BrandCard.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import { ReactElement } from 'react'; interface BrandCardProps { date?: Date; @@ -7,14 +7,14 @@ interface BrandCardProps { // 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" } + { 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) { +export default function BrandCard({ date, className = '' }: BrandCardProps): ReactElement { const isToday = date ? new Date().toDateString() === date.toDateString() : true; // Get a consistent message for each date @@ -28,15 +28,12 @@ export default function BrandCard({ date, className = '' }: BrandCardProps) { const pastMessage = date ? getPastDayMessage(date) : pastDayMessages[0]; return ( -
{/* Logo */} -
+ `} + > @@ -79,7 +79,7 @@ export default function BrandCard({ date, className = '' }: BrandCardProps) { {isToday ? ( <> {/* Today's content */} -

-

- You've got 3 major updates this morning + You've got 3 major updates this morning

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

-

{ // Initialize from localStorage or system preference const savedTheme = localStorage.getItem('theme'); const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - + const shouldBeDark = savedTheme === 'dark' || (!savedTheme && systemPrefersDark); setIsDark(shouldBeDark); - + if (shouldBeDark) { document.documentElement.classList.add('dark'); } @@ -20,7 +21,7 @@ export function DarkModeToggle() { const toggleDarkMode = () => { const newIsDark = !isDark; setIsDark(newIsDark); - + if (newIsDark) { document.documentElement.classList.add('dark'); localStorage.setItem('theme', 'dark'); diff --git a/ui-v2/src/components/DateDisplay.tsx b/ui-v2/src/components/DateDisplay.tsx index 2d05f83d..decb405f 100644 --- a/ui-v2/src/components/DateDisplay.tsx +++ b/ui-v2/src/components/DateDisplay.tsx @@ -1,7 +1,8 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState, ReactElement } from 'react'; + import { useTimeline } from '../contexts/TimelineContext'; -export function DateDisplay() { +export function DateDisplay(): ReactElement { const { currentDate } = useTimeline(); const [displayDate, setDisplayDate] = useState(currentDate); const [isFlipping, setIsFlipping] = useState(false); @@ -17,13 +18,26 @@ export function DateDisplay() { }, [currentDate]); const formatDate = (date: Date) => { - const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + const monthNames = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; - + return { month: monthNames[date.getMonth()], day: date.getDate(), - weekday: dayNames[date.getDay()] + weekday: dayNames[date.getDay()], }; }; @@ -31,7 +45,7 @@ export function DateDisplay() { return (
-
= ({ 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 ( +
+ {/* Rain with enhanced visibility for testing */} +
+ +
+ +
+ ); +}; + +export default GooseLogo; diff --git a/ui-v2/src/components/Home.tsx b/ui-v2/src/components/Home.tsx new file mode 100644 index 00000000..fd5e19c1 --- /dev/null +++ b/ui-v2/src/components/Home.tsx @@ -0,0 +1,12 @@ +import { ReactElement } from 'react'; + +import GooseLogo from '../components/GooseLogo'; + +export default function Home(): ReactElement { + return ( +
+ +

Goose v2

+
+ ); +} diff --git a/ui-v2/src/components/Timeline.tsx b/ui-v2/src/components/Timeline.tsx index 2edfba35..96ceb0b9 100644 --- a/ui-v2/src/components/Timeline.tsx +++ b/ui-v2/src/components/Timeline.tsx @@ -1,11 +1,5 @@ -import React, { useRef, useMemo, useEffect } from 'react'; -import { useTimeline } from '../contexts/TimelineContext'; -import ChartTile from './tiles/ChartTile.tsx'; -import HighlightTile from './tiles/HighlightTile.tsx'; -import PieChartTile from './tiles/PieChartTile.tsx'; -import ListTile from './tiles/ListTile.tsx'; -import ClockTile from './tiles/ClockTile.tsx'; -import TimelineDots from './TimelineDots'; +import { useRef, useMemo, useEffect, ReactElement } from 'react'; + import { ChartLineIcon, ChartBarIcon, @@ -14,6 +8,13 @@ import { StarIcon, TrendingUpIcon, } from './icons'; +import { useTimeline } from '../contexts/TimelineContext'; +import ChartTile from './tiles/ChartTile.tsx'; +import ClockTile from './tiles/ClockTile.tsx'; +import HighlightTile from './tiles/HighlightTile.tsx'; +import ListTile from './tiles/ListTile.tsx'; +import PieChartTile from './tiles/PieChartTile.tsx'; +import TimelineDots from './TimelineDots'; const generateRandomData = (length: number) => Array.from({ length }, () => Math.floor(Math.random() * 100)); @@ -242,7 +243,7 @@ const generateTileData = (date: Date) => { }; }; -export default function Timeline() { +export default function Timeline(): ReactElement { const containerRef = useRef(null); const sectionRefs = useRef<(HTMLDivElement | null)[]>([]); const { setCurrentDate } = useTimeline(); @@ -269,8 +270,11 @@ export default function Timeline() { }, []); // Function to center the timeline in a section - const centerTimeline = (sectionElement: HTMLDivElement, animate: boolean = true) => { - if (!sectionElement) return; + const centerTimeline = ( + sectionElement: HTMLDivElement | null, + animate: boolean = true + ): HTMLDivElement | null => { + if (!sectionElement) return sectionElement; requestAnimationFrame(() => { const totalWidth = sectionElement.scrollWidth; @@ -280,31 +284,37 @@ export default function Timeline() { if (animate) { sectionElement.scrollTo({ left: scrollToX, - behavior: 'smooth' + behavior: 'smooth', }); } else { sectionElement.scrollLeft = scrollToX; } }); + + return sectionElement; }; useEffect(() => { + // Capture ref values at the start of the effect + const currentContainer = containerRef.current; + const currentSections = [...sectionRefs.current]; + // Create the intersection observer const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { const section = entry.target as HTMLDivElement; - + // When section comes into view if (entry.isIntersecting) { // Update current date const sectionIndex = sectionRefs.current.indexOf(section); - if (sectionIndex !== -1) { + if (sectionIndex !== -1 && sections[sectionIndex]) { const date = sections[sectionIndex].date; setCurrentDate(date); } } - + // When section is fully visible and centered if (entry.intersectionRatio > 0.8) { centerTimeline(section, true); @@ -312,15 +322,15 @@ export default function Timeline() { }); }, { - threshold: [0, 0.8, 1], // Track when section is hidden, mostly visible, and fully visible - rootMargin: '-10% 0px', // Slightly reduced margin for more natural triggering + threshold: [0, 0.8, 1], // Track when section is hidden, mostly visible, and fully visible + rootMargin: '-10% 0px', // Slightly reduced margin for more natural triggering } ); // Add scroll handler for even faster updates const handleScroll = () => { - if (!containerRef.current) return; - + if (!currentContainer) return; + // Find the section closest to the middle of the viewport const viewportMiddle = window.innerHeight / 2; let closestSection: HTMLDivElement | null = null; @@ -331,7 +341,7 @@ export default function Timeline() { const rect = section.getBoundingClientRect(); const sectionMiddle = rect.top + rect.height / 2; const distance = Math.abs(sectionMiddle - viewportMiddle); - + if (distance < closestDistance) { closestDistance = distance; closestSection = section; @@ -340,7 +350,7 @@ export default function Timeline() { if (closestSection) { const sectionIndex = sectionRefs.current.indexOf(closestSection); - if (sectionIndex !== -1) { + if (sectionIndex !== -1 && sections[sectionIndex]) { const date = sections[sectionIndex].date; setCurrentDate(date); } @@ -351,13 +361,14 @@ export default function Timeline() { let lastScrollTime = 0; const throttledScrollHandler = () => { const now = Date.now(); - if (now - lastScrollTime >= 150) { // Throttle to ~6-7 times per second + if (now - lastScrollTime >= 150) { + // Throttle to ~6-7 times per second handleScroll(); lastScrollTime = now; } }; - containerRef.current?.addEventListener('scroll', throttledScrollHandler, { passive: true }); + currentContainer?.addEventListener('scroll', throttledScrollHandler, { passive: true }); // Add resize handler const handleResize = () => { @@ -385,30 +396,41 @@ export default function Timeline() { } }); - // Cleanup function + // Cleanup function using captured values return () => { window.removeEventListener('resize', handleResize); - containerRef.current?.removeEventListener('scroll', throttledScrollHandler); - sectionRefs.current.forEach((section) => { + currentContainer?.removeEventListener('scroll', throttledScrollHandler); + currentSections.forEach((section) => { if (section) { observer.unobserve(section); } }); }; - }, []); + }, [sections, setCurrentDate]); - const renderTile = (tile: any, index: number) => { + interface TileProps { + [key: string]: unknown; + } + + interface Tile { + type: string; + props: TileProps; + } + + const renderTile = (tile: Tile, index: number): ReactElement | null => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const props = tile.props as any; // Use any for flexibility with different tile prop types switch (tile.type) { case 'chart': - return ; + return ; case 'highlight': - return ; + return ; case 'pie': - return ; + return ; case 'list': - return ; + return ; case 'clock': - return ; + return ; default: return null; } @@ -422,7 +444,9 @@ export default function Timeline() { {sections.map((section, index) => (
(sectionRefs.current[index] = el)} + ref={(el) => { + sectionRefs.current[index] = el; + }} className="h-screen relative snap-center snap-always overflow-y-hidden overflow-x-scroll snap-x snap-mandatory scrollbar-hide animate-[fadein_300ms_ease-in-out]" >
@@ -455,8 +479,8 @@ export default function Timeline() {
@@ -464,8 +488,8 @@ export default function Timeline() {
@@ -473,8 +497,8 @@ export default function Timeline() {
diff --git a/ui-v2/src/components/TimelineContext.tsx b/ui-v2/src/components/TimelineContext.tsx index 65275961..5d562e45 100644 --- a/ui-v2/src/components/TimelineContext.tsx +++ b/ui-v2/src/components/TimelineContext.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, useState, useCallback } from 'react'; +import { createContext, useContext, useState, useCallback, ReactElement, ReactNode } from 'react'; interface TimelineContextType { currentDate: Date; @@ -8,10 +8,10 @@ interface TimelineContextType { const TimelineContext = createContext(undefined); -export function TimelineProvider({ children }: { children: React.ReactNode }) { +export function TimelineProvider({ children }: { children: ReactNode }): ReactElement { const [currentDate, setCurrentDate] = useState(new Date()); - const isCurrentDate = useCallback((date: Date) => { + const isCurrentDate = useCallback((date: Date): boolean => { return date.toDateString() === new Date().toDateString(); }, []); @@ -22,7 +22,7 @@ export function TimelineProvider({ children }: { children: React.ReactNode }) { ); } -export function useTimeline() { +export function useTimeline(): TimelineContextType { const context = useContext(TimelineContext); if (context === undefined) { throw new Error('useTimeline must be used within a TimelineProvider'); diff --git a/ui-v2/src/components/TimelineDots.tsx b/ui-v2/src/components/TimelineDots.tsx index ef152a76..3260431f 100644 --- a/ui-v2/src/components/TimelineDots.tsx +++ b/ui-v2/src/components/TimelineDots.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import { useMemo, ReactElement } from 'react'; interface TimelineDotsProps { height: number | string; @@ -12,33 +12,43 @@ interface Dot { opacity: number; } -export default function TimelineDots({ height, isUpper = false, isCurrentDay = false }: TimelineDotsProps) { +export default function TimelineDots({ + height, + isUpper = false, + isCurrentDay = false, +}: TimelineDotsProps): ReactElement { // 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); - + 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) { + if (isCluster && clusterPoints.length > 0) { // 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 + if (clusterPoint !== undefined) { + top = clusterPoint + (Math.random() - 0.5) * 15; // ±7.5% variation + } else { + top = Math.random() * 100; + } } 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 @@ -47,17 +57,17 @@ export default function TimelineDots({ height, isUpper = false, isCurrentDay = f } return dots; }; - + return generateDots(); }, []); // Empty dependency array means this only runs once return ( -
{/* Main line */} @@ -71,11 +81,11 @@ export default function TimelineDots({ height, isUpper = false, isCurrentDay = f height: '4px', left: '-1.625px', // Center 4px dot on 0.75px line top: '0', - transform: 'translateY(-50%)' + transform: 'translateY(-50%)', }} /> )} - + {/* Random dots */} {dots.map((dot, index) => (
); -}; +} diff --git a/ui-v2/src/components/ValueCard.tsx b/ui-v2/src/components/ValueCard.tsx index c85c3e18..dfa1d85a 100644 --- a/ui-v2/src/components/ValueCard.tsx +++ b/ui-v2/src/components/ValueCard.tsx @@ -1,11 +1,19 @@ -import React from 'react'; +import { ReactElement } from 'react'; interface BrandCardProps { date?: Date; className?: string; } -export default function BrandCard({ }: BrandCardProps) { +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): ReactElement { const isToday = date ? new Date().toDateString() === date.toDateString() : true; // Get a consistent message for each date @@ -19,34 +27,34 @@ export default function BrandCard({ }: BrandCardProps) { const pastMessage = date ? getPastDayMessage(date) : pastDayMessages[0]; return ( -
{/* Content */}
{/* Logo */} -
+ `} + > @@ -70,7 +78,7 @@ export default function BrandCard({ }: BrandCardProps) { {isToday ? ( <> {/* Today's content */} -

-

- You've got 3 major updates this morning + You've got 3 major updates this morning

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

-

= ({ onTileCreatorToggle }) => { return ( - - + ), - label: "Make a Tile", - color: "bg-[#4F6BFF] hover:bg-[#4F6BFF]/90", + label: 'Make a Tile', + color: 'bg-[#4F6BFF] hover:bg-[#4F6BFF]/90', rotation: -3, }, - { + { icon: ( - + ), - label: "Tasks", - color: "bg-[#E042A5] hover:bg-[#E042A5]/90", + label: 'Tasks', + color: 'bg-[#E042A5] hover:bg-[#E042A5]/90', rotation: 2, }, - { + { icon: ( - + ), - label: "Add", - color: "bg-[#05C168] hover:bg-[#05C168]/90", + label: 'Add', + color: 'bg-[#05C168] hover:bg-[#05C168]/90', rotation: -2, }, - { + { icon: ( - + ), - label: "Issues", - color: "bg-[#FF9900] hover:bg-[#FF9900]/90", + label: 'Issues', + color: 'bg-[#FF9900] hover:bg-[#FF9900]/90', rotation: 3, }, ]; @@ -66,23 +64,23 @@ export const ChatIcons: React.FC = ({ className }) => { if (hoveredIndex === null) return 0; const spread = 16; const centerOffset = hoveredIndex * -spread; - return (index * spread) + centerOffset; + return index * spread + centerOffset; }; return ( setHoveredIndex(index)} onHoverEnd={() => setHoveredIndex(null)} @@ -95,15 +93,13 @@ export const ChatIcons: React.FC = ({ className }) => { flex h-12 w-12 items-center justify-center rounded-xl transition-all duration-200 shadow-sm ${tool.color} - ${hoveredIndex !== null && hoveredIndex !== index ? "opacity-50" : ""} + ${hoveredIndex !== null && hoveredIndex !== index ? 'opacity-50' : ''} `} > {tool.icon} - - {tool.label} - + {tool.label} ); diff --git a/ui-v2/src/components/chat/ChatInput.tsx b/ui-v2/src/components/chat/ChatInput.tsx index 0703f82b..a55330d2 100644 --- a/ui-v2/src/components/chat/ChatInput.tsx +++ b/ui-v2/src/components/chat/ChatInput.tsx @@ -1,4 +1,5 @@ import React, { useState, useRef, useEffect } from 'react'; + import { motion } from 'framer-motion'; interface ChatInputProps { @@ -11,7 +12,7 @@ interface ChatInputProps { export const ChatInput: React.FC = ({ handleSubmit, isLoading = false, - onStop, + onStop: _onStop, initialValue = '', }) => { const [input, setInput] = useState(initialValue); @@ -36,7 +37,7 @@ export const ChatInput: React.FC = ({ const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.attributeName === 'class') { - setKey(prev => prev + 1); // Force textarea to re-render + setKey((prev) => prev + 1); // Force textarea to re-render } }); }); @@ -50,7 +51,7 @@ export const ChatInput: React.FC = ({ const handleFormSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!input.trim() || isLoading) return; - + handleSubmit(e); setInput(''); if (textareaRef.current) { @@ -72,12 +73,12 @@ export const ChatInput: React.FC = ({ className="w-full bg-black dark:bg-white rounded-xl shadow-lg" initial={{ opacity: 0, y: 20, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} - transition={{ - type: "spring", + transition={{ + type: 'spring', stiffness: 300, - damping: 30 + damping: 30, }} - > + >