From 85d19cf1c84243d6fb15e2fbf9c2b330029aebe4 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Thu, 10 Jul 2025 16:14:36 +0200 Subject: [PATCH 01/15] Set up folder structure --- .../{app-content.tsx => Display/AppContent.tsx} | 0 .../{app-header.tsx => Display/AppHeader.tsx} | 12 ++++++------ .../components/{app-logo.tsx => Display/AppLogo.tsx} | 2 +- .../{app-logo-icon.tsx => Display/AppLogoIcon.tsx} | 0 .../{app-shell.tsx => Display/AppShell.tsx} | 0 .../{app-sidebar.tsx => Display/AppSidebar.tsx} | 8 ++++---- .../AppSidebarHeader.tsx} | 2 +- .../{breadcrumbs.tsx => Display/Breadcrumbs.tsx} | 0 .../{nav-footer.tsx => Display/NavFooter.tsx} | 0 .../components/{nav-main.tsx => Display/NavMain.tsx} | 0 .../components/{nav-user.tsx => Display/NavUser.tsx} | 6 +++--- .../{heading-small.tsx => HeadingSmall.tsx} | 0 .../components/{input-error.tsx => InputError.tsx} | 0 .../AppearanceDropdown.tsx} | 2 +- .../AppearanceTabs.tsx} | 0 .../{delete-user.tsx => Settings/DeleteUser.tsx} | 4 ++-- .../{user-info.tsx => Settings/UserInfo.tsx} | 0 .../UserMenuContent.tsx} | 2 +- .../js/components/{text-link.tsx => TextLink.tsx} | 0 .../ui/{dropdown-menu.tsx => DropdownMenu.tsx} | 0 .../ui/{navigation-menu.tsx => NavigationMenu.tsx} | 0 ...laceholder-pattern.tsx => PlaceholderPattern.tsx} | 0 .../ui/{toggle-group.tsx => ToggleGroup.tsx} | 0 resources/js/layouts/app/app-header-layout.tsx | 6 +++--- resources/js/layouts/app/app-sidebar-layout.tsx | 8 ++++---- resources/js/layouts/auth/auth-card-layout.tsx | 2 +- resources/js/layouts/auth/auth-simple-layout.tsx | 2 +- resources/js/layouts/auth/auth-split-layout.tsx | 2 +- resources/js/pages/auth/confirm-password.tsx | 2 +- resources/js/pages/auth/forgot-password.tsx | 4 ++-- resources/js/pages/auth/login.tsx | 4 ++-- resources/js/pages/auth/register.tsx | 4 ++-- resources/js/pages/auth/reset-password.tsx | 2 +- resources/js/pages/auth/verify-email.tsx | 2 +- resources/js/pages/dashboard.tsx | 2 +- resources/js/pages/settings/appearance.tsx | 4 ++-- resources/js/pages/settings/password.tsx | 4 ++-- resources/js/pages/settings/profile.tsx | 6 +++--- 38 files changed, 46 insertions(+), 46 deletions(-) rename resources/js/components/{app-content.tsx => Display/AppContent.tsx} (100%) rename resources/js/components/{app-header.tsx => Display/AppHeader.tsx} (96%) rename resources/js/components/{app-logo.tsx => Display/AppLogo.tsx} (92%) rename resources/js/components/{app-logo-icon.tsx => Display/AppLogoIcon.tsx} (100%) rename resources/js/components/{app-shell.tsx => Display/AppShell.tsx} (100%) rename resources/js/components/{app-sidebar.tsx => Display/AppSidebar.tsx} (88%) rename resources/js/components/{app-sidebar-header.tsx => Display/AppSidebarHeader.tsx} (91%) rename resources/js/components/{breadcrumbs.tsx => Display/Breadcrumbs.tsx} (100%) rename resources/js/components/{nav-footer.tsx => Display/NavFooter.tsx} (100%) rename resources/js/components/{nav-main.tsx => Display/NavMain.tsx} (100%) rename resources/js/components/{nav-user.tsx => Display/NavUser.tsx} (89%) rename resources/js/components/{heading-small.tsx => HeadingSmall.tsx} (100%) rename resources/js/components/{input-error.tsx => InputError.tsx} (100%) rename resources/js/components/{appearance-dropdown.tsx => Settings/AppearanceDropdown.tsx} (97%) rename resources/js/components/{appearance-tabs.tsx => Settings/AppearanceTabs.tsx} (100%) rename resources/js/components/{delete-user.tsx => Settings/DeleteUser.tsx} (97%) rename resources/js/components/{user-info.tsx => Settings/UserInfo.tsx} (100%) rename resources/js/components/{user-menu-content.tsx => Settings/UserMenuContent.tsx} (96%) rename resources/js/components/{text-link.tsx => TextLink.tsx} (100%) rename resources/js/components/ui/{dropdown-menu.tsx => DropdownMenu.tsx} (100%) rename resources/js/components/ui/{navigation-menu.tsx => NavigationMenu.tsx} (100%) rename resources/js/components/ui/{placeholder-pattern.tsx => PlaceholderPattern.tsx} (100%) rename resources/js/components/ui/{toggle-group.tsx => ToggleGroup.tsx} (100%) diff --git a/resources/js/components/app-content.tsx b/resources/js/components/Display/AppContent.tsx similarity index 100% rename from resources/js/components/app-content.tsx rename to resources/js/components/Display/AppContent.tsx diff --git a/resources/js/components/app-header.tsx b/resources/js/components/Display/AppHeader.tsx similarity index 96% rename from resources/js/components/app-header.tsx rename to resources/js/components/Display/AppHeader.tsx index 831f2c3..2bc1fe4 100644 --- a/resources/js/components/app-header.tsx +++ b/resources/js/components/Display/AppHeader.tsx @@ -1,19 +1,19 @@ -import { Breadcrumbs } from '@/components/breadcrumbs'; +import { Breadcrumbs } from '@/components/Display/Breadcrumbs'; import { Icon } from '@/components/icon'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Button } from '@/components/ui/button'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; -import { NavigationMenu, NavigationMenuItem, NavigationMenuList, navigationMenuTriggerStyle } from '@/components/ui/navigation-menu'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/DropdownMenu'; +import { NavigationMenu, NavigationMenuItem, NavigationMenuList, navigationMenuTriggerStyle } from '@/components/ui/NavigationMenu'; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { UserMenuContent } from '@/components/user-menu-content'; +import { UserMenuContent } from '@/components/Settings/UserMenuContent'; import { useInitials } from '@/hooks/use-initials'; import { cn } from '@/lib/utils'; import { type BreadcrumbItem, type NavItem, type SharedData } from '@/types'; import { Link, usePage } from '@inertiajs/react'; import { BookOpen, Folder, LayoutGrid, Menu, Search } from 'lucide-react'; -import AppLogo from './app-logo'; -import AppLogoIcon from './app-logo-icon'; +import AppLogo from './AppLogo'; +import AppLogoIcon from './AppLogoIcon'; const mainNavItems: NavItem[] = [ { diff --git a/resources/js/components/app-logo.tsx b/resources/js/components/Display/AppLogo.tsx similarity index 92% rename from resources/js/components/app-logo.tsx rename to resources/js/components/Display/AppLogo.tsx index 69bdcb8..6c377cf 100644 --- a/resources/js/components/app-logo.tsx +++ b/resources/js/components/Display/AppLogo.tsx @@ -1,4 +1,4 @@ -import AppLogoIcon from './app-logo-icon'; +import AppLogoIcon from './AppLogoIcon'; export default function AppLogo() { return ( diff --git a/resources/js/components/app-logo-icon.tsx b/resources/js/components/Display/AppLogoIcon.tsx similarity index 100% rename from resources/js/components/app-logo-icon.tsx rename to resources/js/components/Display/AppLogoIcon.tsx diff --git a/resources/js/components/app-shell.tsx b/resources/js/components/Display/AppShell.tsx similarity index 100% rename from resources/js/components/app-shell.tsx rename to resources/js/components/Display/AppShell.tsx diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/Display/AppSidebar.tsx similarity index 88% rename from resources/js/components/app-sidebar.tsx rename to resources/js/components/Display/AppSidebar.tsx index c517672..b2151b8 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/Display/AppSidebar.tsx @@ -1,11 +1,11 @@ -import { NavFooter } from '@/components/nav-footer'; -import { NavMain } from '@/components/nav-main'; -import { NavUser } from '@/components/nav-user'; +import { NavFooter } from '@/components/Display/NavFooter'; +import { NavMain } from '@/components/Display/NavMain'; +import { NavUser } from '@/components/Display/NavUser'; import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'; import { type NavItem } from '@/types'; import { Link } from '@inertiajs/react'; import { BookOpen, Folder, LayoutGrid } from 'lucide-react'; -import AppLogo from './app-logo'; +import AppLogo from './AppLogo'; const mainNavItems: NavItem[] = [ { diff --git a/resources/js/components/app-sidebar-header.tsx b/resources/js/components/Display/AppSidebarHeader.tsx similarity index 91% rename from resources/js/components/app-sidebar-header.tsx rename to resources/js/components/Display/AppSidebarHeader.tsx index 6a3128b..10c7da2 100644 --- a/resources/js/components/app-sidebar-header.tsx +++ b/resources/js/components/Display/AppSidebarHeader.tsx @@ -1,4 +1,4 @@ -import { Breadcrumbs } from '@/components/breadcrumbs'; +import { Breadcrumbs } from '@/components/Display/Breadcrumbs'; import { SidebarTrigger } from '@/components/ui/sidebar'; import { type BreadcrumbItem as BreadcrumbItemType } from '@/types'; diff --git a/resources/js/components/breadcrumbs.tsx b/resources/js/components/Display/Breadcrumbs.tsx similarity index 100% rename from resources/js/components/breadcrumbs.tsx rename to resources/js/components/Display/Breadcrumbs.tsx diff --git a/resources/js/components/nav-footer.tsx b/resources/js/components/Display/NavFooter.tsx similarity index 100% rename from resources/js/components/nav-footer.tsx rename to resources/js/components/Display/NavFooter.tsx diff --git a/resources/js/components/nav-main.tsx b/resources/js/components/Display/NavMain.tsx similarity index 100% rename from resources/js/components/nav-main.tsx rename to resources/js/components/Display/NavMain.tsx diff --git a/resources/js/components/nav-user.tsx b/resources/js/components/Display/NavUser.tsx similarity index 89% rename from resources/js/components/nav-user.tsx rename to resources/js/components/Display/NavUser.tsx index 386be8f..7918dfc 100644 --- a/resources/js/components/nav-user.tsx +++ b/resources/js/components/Display/NavUser.tsx @@ -1,7 +1,7 @@ -import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/DropdownMenu'; import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar'; -import { UserInfo } from '@/components/user-info'; -import { UserMenuContent } from '@/components/user-menu-content'; +import { UserInfo } from '@/components/Settings/UserInfo'; +import { UserMenuContent } from '@/components/Settings/UserMenuContent'; import { useIsMobile } from '@/hooks/use-mobile'; import { type SharedData } from '@/types'; import { usePage } from '@inertiajs/react'; diff --git a/resources/js/components/heading-small.tsx b/resources/js/components/HeadingSmall.tsx similarity index 100% rename from resources/js/components/heading-small.tsx rename to resources/js/components/HeadingSmall.tsx diff --git a/resources/js/components/input-error.tsx b/resources/js/components/InputError.tsx similarity index 100% rename from resources/js/components/input-error.tsx rename to resources/js/components/InputError.tsx diff --git a/resources/js/components/appearance-dropdown.tsx b/resources/js/components/Settings/AppearanceDropdown.tsx similarity index 97% rename from resources/js/components/appearance-dropdown.tsx rename to resources/js/components/Settings/AppearanceDropdown.tsx index 89a4586..ee5032f 100644 --- a/resources/js/components/appearance-dropdown.tsx +++ b/resources/js/components/Settings/AppearanceDropdown.tsx @@ -1,5 +1,5 @@ import { Button } from '@/components/ui/button'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/DropdownMenu'; import { useAppearance } from '@/hooks/use-appearance'; import { Monitor, Moon, Sun } from 'lucide-react'; import { HTMLAttributes } from 'react'; diff --git a/resources/js/components/appearance-tabs.tsx b/resources/js/components/Settings/AppearanceTabs.tsx similarity index 100% rename from resources/js/components/appearance-tabs.tsx rename to resources/js/components/Settings/AppearanceTabs.tsx diff --git a/resources/js/components/delete-user.tsx b/resources/js/components/Settings/DeleteUser.tsx similarity index 97% rename from resources/js/components/delete-user.tsx rename to resources/js/components/Settings/DeleteUser.tsx index e1f8788..976fae0 100644 --- a/resources/js/components/delete-user.tsx +++ b/resources/js/components/Settings/DeleteUser.tsx @@ -1,12 +1,12 @@ import { useForm } from '@inertiajs/react'; import { FormEventHandler, useRef } from 'react'; -import InputError from '@/components/input-error'; +import InputError from '@/components/InputError'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import HeadingSmall from '@/components/heading-small'; +import HeadingSmall from '@/components/HeadingSmall'; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; diff --git a/resources/js/components/user-info.tsx b/resources/js/components/Settings/UserInfo.tsx similarity index 100% rename from resources/js/components/user-info.tsx rename to resources/js/components/Settings/UserInfo.tsx diff --git a/resources/js/components/user-menu-content.tsx b/resources/js/components/Settings/UserMenuContent.tsx similarity index 96% rename from resources/js/components/user-menu-content.tsx rename to resources/js/components/Settings/UserMenuContent.tsx index c002b19..627d898 100644 --- a/resources/js/components/user-menu-content.tsx +++ b/resources/js/components/Settings/UserMenuContent.tsx @@ -1,5 +1,5 @@ import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from '@/components/ui/dropdown-menu'; -import { UserInfo } from '@/components/user-info'; +import { UserInfo } from '@/components/Settings/UserInfo'; import { useMobileNavigation } from '@/hooks/use-mobile-navigation'; import { type User } from '@/types'; import { Link, router } from '@inertiajs/react'; diff --git a/resources/js/components/text-link.tsx b/resources/js/components/TextLink.tsx similarity index 100% rename from resources/js/components/text-link.tsx rename to resources/js/components/TextLink.tsx diff --git a/resources/js/components/ui/dropdown-menu.tsx b/resources/js/components/ui/DropdownMenu.tsx similarity index 100% rename from resources/js/components/ui/dropdown-menu.tsx rename to resources/js/components/ui/DropdownMenu.tsx diff --git a/resources/js/components/ui/navigation-menu.tsx b/resources/js/components/ui/NavigationMenu.tsx similarity index 100% rename from resources/js/components/ui/navigation-menu.tsx rename to resources/js/components/ui/NavigationMenu.tsx diff --git a/resources/js/components/ui/placeholder-pattern.tsx b/resources/js/components/ui/PlaceholderPattern.tsx similarity index 100% rename from resources/js/components/ui/placeholder-pattern.tsx rename to resources/js/components/ui/PlaceholderPattern.tsx diff --git a/resources/js/components/ui/toggle-group.tsx b/resources/js/components/ui/ToggleGroup.tsx similarity index 100% rename from resources/js/components/ui/toggle-group.tsx rename to resources/js/components/ui/ToggleGroup.tsx diff --git a/resources/js/layouts/app/app-header-layout.tsx b/resources/js/layouts/app/app-header-layout.tsx index 9cde5da..feda5e5 100644 --- a/resources/js/layouts/app/app-header-layout.tsx +++ b/resources/js/layouts/app/app-header-layout.tsx @@ -1,6 +1,6 @@ -import { AppContent } from '@/components/app-content'; -import { AppHeader } from '@/components/app-header'; -import { AppShell } from '@/components/app-shell'; +import { AppContent } from '@/components/Display/AppContent'; +import { AppHeader } from '@/components/Display/AppHeader'; +import { AppShell } from '@/components/Display/AppShell'; import { type BreadcrumbItem } from '@/types'; import type { PropsWithChildren } from 'react'; diff --git a/resources/js/layouts/app/app-sidebar-layout.tsx b/resources/js/layouts/app/app-sidebar-layout.tsx index 7fa11d1..7fe141b 100644 --- a/resources/js/layouts/app/app-sidebar-layout.tsx +++ b/resources/js/layouts/app/app-sidebar-layout.tsx @@ -1,7 +1,7 @@ -import { AppContent } from '@/components/app-content'; -import { AppShell } from '@/components/app-shell'; -import { AppSidebar } from '@/components/app-sidebar'; -import { AppSidebarHeader } from '@/components/app-sidebar-header'; +import { AppContent } from '@/components/Display/AppContent'; +import { AppShell } from '@/components/Display/AppShell'; +import { AppSidebar } from '@/components/Display/AppSidebar'; +import { AppSidebarHeader } from '@/components/Display/AppSidebarHeader'; import { type BreadcrumbItem } from '@/types'; import { type PropsWithChildren } from 'react'; diff --git a/resources/js/layouts/auth/auth-card-layout.tsx b/resources/js/layouts/auth/auth-card-layout.tsx index d0bfc4e..72d09cc 100644 --- a/resources/js/layouts/auth/auth-card-layout.tsx +++ b/resources/js/layouts/auth/auth-card-layout.tsx @@ -1,4 +1,4 @@ -import AppLogoIcon from '@/components/app-logo-icon'; +import AppLogoIcon from '@/components/Display/AppLogoIcon'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Link } from '@inertiajs/react'; import { type PropsWithChildren } from 'react'; diff --git a/resources/js/layouts/auth/auth-simple-layout.tsx b/resources/js/layouts/auth/auth-simple-layout.tsx index 6de3efc..7383ee5 100644 --- a/resources/js/layouts/auth/auth-simple-layout.tsx +++ b/resources/js/layouts/auth/auth-simple-layout.tsx @@ -1,4 +1,4 @@ -import AppLogoIcon from '@/components/app-logo-icon'; +import AppLogoIcon from '@/components/Display/AppLogoIcon'; import { Link } from '@inertiajs/react'; import { type PropsWithChildren } from 'react'; diff --git a/resources/js/layouts/auth/auth-split-layout.tsx b/resources/js/layouts/auth/auth-split-layout.tsx index 03daf1c..6a47bbf 100644 --- a/resources/js/layouts/auth/auth-split-layout.tsx +++ b/resources/js/layouts/auth/auth-split-layout.tsx @@ -1,4 +1,4 @@ -import AppLogoIcon from '@/components/app-logo-icon'; +import AppLogoIcon from '@/components/Display/AppLogoIcon'; import { type SharedData } from '@/types'; import { Link, usePage } from '@inertiajs/react'; import { type PropsWithChildren } from 'react'; diff --git a/resources/js/pages/auth/confirm-password.tsx b/resources/js/pages/auth/confirm-password.tsx index bc1ae57..50907b9 100644 --- a/resources/js/pages/auth/confirm-password.tsx +++ b/resources/js/pages/auth/confirm-password.tsx @@ -3,7 +3,7 @@ import { Head, useForm } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; import { FormEventHandler } from 'react'; -import InputError from '@/components/input-error'; +import InputError from '@/components/InputError'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; diff --git a/resources/js/pages/auth/forgot-password.tsx b/resources/js/pages/auth/forgot-password.tsx index 86d1b30..a69c3bc 100644 --- a/resources/js/pages/auth/forgot-password.tsx +++ b/resources/js/pages/auth/forgot-password.tsx @@ -3,8 +3,8 @@ import { Head, useForm } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; import { FormEventHandler } from 'react'; -import InputError from '@/components/input-error'; -import TextLink from '@/components/text-link'; +import InputError from '@/components/InputError'; +import TextLink from '@/components/TextLink'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; diff --git a/resources/js/pages/auth/login.tsx b/resources/js/pages/auth/login.tsx index 28f76f0..8df7bfd 100644 --- a/resources/js/pages/auth/login.tsx +++ b/resources/js/pages/auth/login.tsx @@ -2,8 +2,8 @@ import { Head, useForm } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; import { FormEventHandler } from 'react'; -import InputError from '@/components/input-error'; -import TextLink from '@/components/text-link'; +import InputError from '@/components/InputError'; +import TextLink from '@/components/TextLink'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; diff --git a/resources/js/pages/auth/register.tsx b/resources/js/pages/auth/register.tsx index 6b0faa8..35091ba 100644 --- a/resources/js/pages/auth/register.tsx +++ b/resources/js/pages/auth/register.tsx @@ -2,8 +2,8 @@ import { Head, useForm } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; import { FormEventHandler } from 'react'; -import InputError from '@/components/input-error'; -import TextLink from '@/components/text-link'; +import InputError from '@/components/InputError'; +import TextLink from '@/components/TextLink'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; diff --git a/resources/js/pages/auth/reset-password.tsx b/resources/js/pages/auth/reset-password.tsx index 8ea5303..816b11d 100644 --- a/resources/js/pages/auth/reset-password.tsx +++ b/resources/js/pages/auth/reset-password.tsx @@ -2,7 +2,7 @@ import { Head, useForm } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; import { FormEventHandler } from 'react'; -import InputError from '@/components/input-error'; +import InputError from '@/components/InputError'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; diff --git a/resources/js/pages/auth/verify-email.tsx b/resources/js/pages/auth/verify-email.tsx index b4f7846..ec9a5ce 100644 --- a/resources/js/pages/auth/verify-email.tsx +++ b/resources/js/pages/auth/verify-email.tsx @@ -3,7 +3,7 @@ import { Head, useForm } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; import { FormEventHandler } from 'react'; -import TextLink from '@/components/text-link'; +import TextLink from '@/components/TextLink'; import { Button } from '@/components/ui/button'; import AuthLayout from '@/layouts/auth-layout'; diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index 3f73f02..822c3dc 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -1,4 +1,4 @@ -import { PlaceholderPattern } from '@/components/ui/placeholder-pattern'; +import { PlaceholderPattern } from '@/components/ui/PlaceholderPattern'; import AppLayout from '@/layouts/app-layout'; import { type BreadcrumbItem } from '@/types'; import { Head } from '@inertiajs/react'; diff --git a/resources/js/pages/settings/appearance.tsx b/resources/js/pages/settings/appearance.tsx index 5099b25..94ab15a 100644 --- a/resources/js/pages/settings/appearance.tsx +++ b/resources/js/pages/settings/appearance.tsx @@ -1,7 +1,7 @@ import { Head } from '@inertiajs/react'; -import AppearanceTabs from '@/components/appearance-tabs'; -import HeadingSmall from '@/components/heading-small'; +import AppearanceTabs from '@/components/Settings/AppearanceTabs'; +import HeadingSmall from '@/components/HeadingSmall'; import { type BreadcrumbItem } from '@/types'; import AppLayout from '@/layouts/app-layout'; diff --git a/resources/js/pages/settings/password.tsx b/resources/js/pages/settings/password.tsx index 43540bb..4d16221 100644 --- a/resources/js/pages/settings/password.tsx +++ b/resources/js/pages/settings/password.tsx @@ -1,4 +1,4 @@ -import InputError from '@/components/input-error'; +import InputError from '@/components/InputError'; import AppLayout from '@/layouts/app-layout'; import SettingsLayout from '@/layouts/settings/layout'; import { type BreadcrumbItem } from '@/types'; @@ -6,7 +6,7 @@ import { Transition } from '@headlessui/react'; import { Head, useForm } from '@inertiajs/react'; import { FormEventHandler, useRef } from 'react'; -import HeadingSmall from '@/components/heading-small'; +import HeadingSmall from '@/components/HeadingSmall'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; diff --git a/resources/js/pages/settings/profile.tsx b/resources/js/pages/settings/profile.tsx index 3aeed3a..25b56be 100644 --- a/resources/js/pages/settings/profile.tsx +++ b/resources/js/pages/settings/profile.tsx @@ -3,9 +3,9 @@ import { Transition } from '@headlessui/react'; import { Head, Link, useForm, usePage } from '@inertiajs/react'; import { FormEventHandler } from 'react'; -import DeleteUser from '@/components/delete-user'; -import HeadingSmall from '@/components/heading-small'; -import InputError from '@/components/input-error'; +import DeleteUser from '@/components/Settings/DeleteUser'; +import HeadingSmall from '@/components/HeadingSmall'; +import InputError from '@/components/InputError'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; From 52f8ae2fd1f63a8c6cf0a8e6514db7dfbdc4f54d Mon Sep 17 00:00:00 2001 From: myrmidex Date: Thu, 10 Jul 2025 17:20:48 +0200 Subject: [PATCH 02/15] Transactions --- .../Transactions/PurchaseController.php | 69 ++++++++++ app/Models/Transactions/Purchase.php | 52 ++++++++ ...25_07_10_150549_create_purchases_table.php | 33 +++++ .../Transactions/AddPurchaseForm.tsx | 126 ++++++++++++++++++ routes/web.php | 15 ++- 5 files changed, 291 insertions(+), 4 deletions(-) create mode 100644 app/Http/Controllers/Transactions/PurchaseController.php create mode 100644 app/Models/Transactions/Purchase.php create mode 100644 database/migrations/2025_07_10_150549_create_purchases_table.php create mode 100644 resources/js/components/Transactions/AddPurchaseForm.tsx diff --git a/app/Http/Controllers/Transactions/PurchaseController.php b/app/Http/Controllers/Transactions/PurchaseController.php new file mode 100644 index 0000000..48925a4 --- /dev/null +++ b/app/Http/Controllers/Transactions/PurchaseController.php @@ -0,0 +1,69 @@ +get(); + + return response()->json($purchases); + } + + public function store(Request $request): \Illuminate\Http\RedirectResponse + { + $validated = $request->validate([ + 'date' => 'required|date|before_or_equal:today', + 'shares' => 'required|numeric|min:0.000001', + 'price_per_share' => 'required|numeric|min:0.01', + 'total_cost' => 'required|numeric|min:0.01', + ]); + + // Verify calculation is correct + $calculatedTotal = $validated['shares'] * $validated['price_per_share']; + if (abs($calculatedTotal - $validated['total_cost']) > 0.01) { + return back()->withErrors([ + 'total_cost' => 'Total cost does not match shares × price per share.' + ]); + } + + Purchase::create([ + 'date' => $validated['date'], + 'shares' => $validated['shares'], + 'price_per_share' => $validated['price_per_share'], + 'total_cost' => $validated['total_cost'], + ]); + + return Redirect::back()->with('success', 'Purchase added successfully!'); + } + + public function summary() + { + $totalShares = Purchase::totalShares(); + $totalInvestment = Purchase::totalInvestment(); + $averageCost = Purchase::averageCostPerShare(); + + return response()->json([ + 'total_shares' => $totalShares, + 'total_investment' => $totalInvestment, + 'average_cost_per_share' => $averageCost, + ]); + } + + /** + * Remove the specified purchase. + */ + public function destroy(Purchase $purchase) + { + $purchase->delete(); + + return Redirect::back()->with('success', 'Purchase deleted successfully!'); + } +} diff --git a/app/Models/Transactions/Purchase.php b/app/Models/Transactions/Purchase.php new file mode 100644 index 0000000..693b8ce --- /dev/null +++ b/app/Models/Transactions/Purchase.php @@ -0,0 +1,52 @@ + 'date', + 'shares' => 'decimal:6', + 'price_per_share' => 'decimal:4', + 'total_cost' => 'decimal:2', + ]; + + /** + * Calculate total shares + */ + public static function totalShares(): float + { + return static::sum('shares'); + } + + /** + * Calculate total investment + */ + public static function totalInvestment(): float + { + return static::sum('total_cost'); + } + + /** + * Get average cost per share + */ + public static function averageCostPerShare(): float + { + $totalShares = static::totalShares(); + $totalCost = static::totalInvestment(); + + return $totalShares > 0 ? $totalCost / $totalShares : 0; + } +} \ No newline at end of file diff --git a/database/migrations/2025_07_10_150549_create_purchases_table.php b/database/migrations/2025_07_10_150549_create_purchases_table.php new file mode 100644 index 0000000..edfc344 --- /dev/null +++ b/database/migrations/2025_07_10_150549_create_purchases_table.php @@ -0,0 +1,33 @@ +id(); + $table->date('date'); + $table->decimal('shares', 12, 6); // Supports fractional shares + $table->decimal('price_per_share', 8, 4); // Price in euros + $table->decimal('total_cost', 12, 2); // Total cost in euros + $table->timestamps(); + + $table->index('date'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('purchases'); + } +}; diff --git a/resources/js/components/Transactions/AddPurchaseForm.tsx b/resources/js/components/Transactions/AddPurchaseForm.tsx new file mode 100644 index 0000000..025b87a --- /dev/null +++ b/resources/js/components/Transactions/AddPurchaseForm.tsx @@ -0,0 +1,126 @@ +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import InputError from '@/components/InputError'; +import { useForm } from '@inertiajs/react'; +import { LoaderCircle } from 'lucide-react'; +import { FormEventHandler, useEffect } from 'react'; + +interface PurchaseFormData { + date: string; + shares: string; + price_per_share: string; + total_cost: string; +} + +export default function AddPurchaseForm() { + const { data, setData, post, processing, errors, reset } = useForm({ + date: new Date().toISOString().split('T')[0], // Today's date in YYYY-MM-DD format + shares: '', + price_per_share: '', + total_cost: '', + }); + + // Auto-calculate total cost when shares or price changes + useEffect(() => { + if (data.shares && data.price_per_share) { + const shares = parseFloat(data.shares); + const pricePerShare = parseFloat(data.price_per_share); + + if (!isNaN(shares) && !isNaN(pricePerShare)) { + const totalCost = (shares * pricePerShare).toFixed(2); + setData('total_cost', totalCost); + } + } + }, [data.shares, data.price_per_share, setData]); + + const submit: FormEventHandler = (e) => { + e.preventDefault(); + + post(route('purchases.store'), { + onSuccess: () => { + reset(); + setData('date', new Date().toISOString().split('T')[0]); + }, + }); + }; + + return ( + + + Add VWCE Purchase + + +
+
+ + setData('date', e.target.value)} + max={new Date().toISOString().split('T')[0]} + /> + +
+ +
+ + setData('shares', e.target.value)} + /> + +
+ +
+ + setData('price_per_share', e.target.value)} + /> + +
+ +
+ + setData('total_cost', e.target.value)} + className="bg-neutral-50 dark:bg-neutral-800" + /> +

+ Auto-calculated from shares × price +

+ +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 5e4cebd..2743eaa 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,5 +1,6 @@ name('home'); -Route::middleware(['auth', 'verified'])->group(function () { - Route::get('dashboard', function () { - return Inertia::render('dashboard'); - })->name('dashboard'); +Route::get('dashboard', function () { + return Inertia::render('dashboard'); +})->name('dashboard'); + +// Purchase routes +Route::prefix('purchases')->name('purchases.')->group(function () { + Route::get('/', [PurchaseController::class, 'index'])->name('index'); + Route::post('/', [PurchaseController::class, 'store'])->name('store'); + Route::get('/summary', [PurchaseController::class, 'summary'])->name('summary'); + Route::delete('/{purchase}', [PurchaseController::class, 'destroy'])->name('destroy'); }); require __DIR__.'/settings.php'; From 12c377c92c4f2555a5ab2ed9c95966c795fc59ff Mon Sep 17 00:00:00 2001 From: myrmidex Date: Thu, 10 Jul 2025 17:37:30 +0200 Subject: [PATCH 03/15] Pricing --- .../Controllers/Pricing/PricingController.php | 54 +++++++++++ app/Models/Pricing/AssetPrice.php | 60 ++++++++++++ ...07_10_152716_create_asset_prices_table.php | 26 ++++++ .../js/components/Pricing/UpdatePriceForm.tsx | 91 +++++++++++++++++++ routes/web.php | 9 ++ 5 files changed, 240 insertions(+) create mode 100644 app/Http/Controllers/Pricing/PricingController.php create mode 100644 app/Models/Pricing/AssetPrice.php create mode 100644 database/migrations/2025_07_10_152716_create_asset_prices_table.php create mode 100644 resources/js/components/Pricing/UpdatePriceForm.tsx diff --git a/app/Http/Controllers/Pricing/PricingController.php b/app/Http/Controllers/Pricing/PricingController.php new file mode 100644 index 0000000..cdd34a3 --- /dev/null +++ b/app/Http/Controllers/Pricing/PricingController.php @@ -0,0 +1,54 @@ +json([ + 'current_price' => $price, + ]); + } + + public function update(Request $request): JsonResponse + { + $validated = $request->validate([ + 'date' => 'required|date|before_or_equal:today', + 'price' => 'required|numeric|min:0.0001', + ]); + + $assetPrice = AssetPrice::updatePrice($validated['date'], $validated['price']); + + return response()->json([ + 'success' => true, + 'message' => 'Asset price updated successfully!', + 'data' => $assetPrice, + ]); + } + + public function history(Request $request): JsonResponse + { + $limit = $request->get('limit', 30); + $history = AssetPrice::history($limit); + + return response()->json($history); + } + + public function forDate(Request $request, string $date): JsonResponse + { + $price = AssetPrice::forDate($date); + + return response()->json([ + 'date' => $date, + 'price' => $price, + ]); + } +} diff --git a/app/Models/Pricing/AssetPrice.php b/app/Models/Pricing/AssetPrice.php new file mode 100644 index 0000000..2d3b6cf --- /dev/null +++ b/app/Models/Pricing/AssetPrice.php @@ -0,0 +1,60 @@ + 'date', + 'price' => 'decimal:4', + ]; + + public static function current(): ?float + { + $latestPrice = static::latest('date')->first(); + + return $latestPrice ? $latestPrice->price : null; + } + + public static function forDate(string $date): ?float + { + $price = static::where('date', '<=', $date) + ->orderBy('date', 'desc') + ->first(); + + return $price ? $price->price : null; + } + + public static function updatePrice(string $date, float $price): self + { + return static::updateOrCreate( + ['date' => $date], + ['price' => $price] + ); + } + + public static function history(int $limit = 30): Collection + { + return static::orderBy('date', 'desc')->limit($limit)->get(); + } +} diff --git a/database/migrations/2025_07_10_152716_create_asset_prices_table.php b/database/migrations/2025_07_10_152716_create_asset_prices_table.php new file mode 100644 index 0000000..94ebab3 --- /dev/null +++ b/database/migrations/2025_07_10_152716_create_asset_prices_table.php @@ -0,0 +1,26 @@ +id(); + $table->date('date'); + $table->decimal('price', 10, 4); + $table->timestamps(); + + $table->unique('date'); + $table->index('date'); + }); + } + + public function down(): void + { + Schema::dropIfExists('asset_prices'); + } +}; diff --git a/resources/js/components/Pricing/UpdatePriceForm.tsx b/resources/js/components/Pricing/UpdatePriceForm.tsx new file mode 100644 index 0000000..b4944e5 --- /dev/null +++ b/resources/js/components/Pricing/UpdatePriceForm.tsx @@ -0,0 +1,91 @@ +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import InputError from '@/components/InputError'; +import { useForm } from '@inertiajs/react'; +import { LoaderCircle } from 'lucide-react'; +import { FormEventHandler } from 'react'; + +interface PriceUpdateFormData { + date: string; + price: string; + [key: string]: string; +} + +interface UpdatePriceFormProps { + currentPrice?: number; + className?: string; +} + +export default function UpdatePriceForm({ currentPrice, className }: UpdatePriceFormProps) { + const { data, setData, post, processing, errors } = useForm({ + date: new Date().toISOString().split('T')[0], // Today's date in YYYY-MM-DD format + price: currentPrice?.toString() || '', + }); + + const submit: FormEventHandler = (e) => { + e.preventDefault(); + + post(route('pricing.update'), { + onSuccess: () => { + // Keep the date, reset only price if needed + // User might want to update same day multiple times + }, + }); + }; + + return ( + + + Update Asset Price + {currentPrice && ( +

+ Current price: €{currentPrice.toFixed(4)} +

+ )} +
+ +
+
+ + setData('date', e.target.value)} + max={new Date().toISOString().split('T')[0]} + /> + +
+ +
+ + setData('price', e.target.value)} + /> +

+ Price per unit/share of the asset +

+ +
+ + +
+
+
+ ); +} diff --git a/routes/web.php b/routes/web.php index 2743eaa..59d6907 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,7 @@ name('destroy'); }); +// Pricing routes +Route::prefix('pricing')->name('pricing.')->group(function () { + Route::get('/current', [PricingController::class, 'current'])->name('current'); + Route::post('/update', [PricingController::class, 'update'])->name('update'); + Route::get('/history', [PricingController::class, 'history'])->name('history'); + Route::get('/date/{date}', [PricingController::class, 'forDate'])->name('for-date'); +}); + require __DIR__.'/settings.php'; require __DIR__.'/auth.php'; From b469423d813509838584d08bd8da790966eb007e Mon Sep 17 00:00:00 2001 From: myrmidex Date: Thu, 10 Jul 2025 18:04:58 +0200 Subject: [PATCH 04/15] Basic display --- .../Transactions/PurchaseController.php | 12 +- resources/css/app.css | 3 + .../js/components/Display/LedCounter.tsx | 298 ++++++++++++++++++ .../Display/MilestoneProgressBar.tsx | 167 ++++++++++ .../js/components/Display/PurchaseModal.tsx | 33 ++ .../js/components/Display/StatsPanel.tsx | 193 ++++++++++++ .../Transactions/AddPurchaseForm.tsx | 38 ++- resources/js/pages/dashboard.tsx | 159 ++++++++-- resources/views/app.blade.php | 2 +- routes/web.php | 2 +- 10 files changed, 860 insertions(+), 47 deletions(-) create mode 100644 resources/js/components/Display/LedCounter.tsx create mode 100644 resources/js/components/Display/MilestoneProgressBar.tsx create mode 100644 resources/js/components/Display/PurchaseModal.tsx create mode 100644 resources/js/components/Display/StatsPanel.tsx diff --git a/app/Http/Controllers/Transactions/PurchaseController.php b/app/Http/Controllers/Transactions/PurchaseController.php index 48925a4..b0bd1a1 100644 --- a/app/Http/Controllers/Transactions/PurchaseController.php +++ b/app/Http/Controllers/Transactions/PurchaseController.php @@ -17,7 +17,7 @@ public function index(): JsonResponse return response()->json($purchases); } - public function store(Request $request): \Illuminate\Http\RedirectResponse + public function store(Request $request): JsonResponse { $validated = $request->validate([ 'date' => 'required|date|before_or_equal:today', @@ -41,7 +41,10 @@ public function store(Request $request): \Illuminate\Http\RedirectResponse 'total_cost' => $validated['total_cost'], ]); - return Redirect::back()->with('success', 'Purchase added successfully!'); + return response()->json([ + 'success' => true, + 'message' => 'Purchase added successfully!', + ]); } public function summary() @@ -64,6 +67,9 @@ public function destroy(Purchase $purchase) { $purchase->delete(); - return Redirect::back()->with('success', 'Purchase deleted successfully!'); + return response()->json([ + 'success' => true, + 'message' => 'Purchase deleted successfully!', + ]); } } diff --git a/resources/css/app.css b/resources/css/app.css index 43fdb4a..2b360a8 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -10,6 +10,9 @@ @theme { --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + + --font-mono-display: + 'Major Mono Display', ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace; --radius-lg: var(--radius); --radius-md: calc(var(--radius) - 2px); diff --git a/resources/js/components/Display/LedCounter.tsx b/resources/js/components/Display/LedCounter.tsx new file mode 100644 index 0000000..85ed4b3 --- /dev/null +++ b/resources/js/components/Display/LedCounter.tsx @@ -0,0 +1,298 @@ +import { cn } from '@/lib/utils'; +import { ChevronLeft, ChevronRight, Plus } from 'lucide-react'; +import { useEffect, useState } from 'react'; + +interface LedCounterProps { + value: number; + className?: string; + animate?: boolean; + currentPrice?: number; + onHover?: (isHovered: boolean) => void; + // Progress bar props + onStatsToggle?: () => void; + showStats?: boolean; + onAddPurchase?: () => void; +} + +export default function LedCounter({ + value, + className, + animate = true, + currentPrice, + onHover, + onStatsToggle, + showStats = false, + onAddPurchase +}: LedCounterProps) { + const [displayValue, setDisplayValue] = useState(0); + const [isHovered, setIsHovered] = useState(false); + const [hoverTimeout, setHoverTimeout] = useState(null); + const [currentMilestoneIndex, setCurrentMilestoneIndex] = useState(0); + + // Milestone definitions + const milestones = [ + { target: 1500, label: '1.5K', color: 'bg-blue-500' }, + { target: 3000, label: '3K', color: 'bg-green-500' }, + { target: 4500, label: '4.5K', color: 'bg-yellow-500' }, + { target: 6000, label: '6K', color: 'bg-red-500' }, + ]; + + const currentMilestone = milestones[currentMilestoneIndex]; + const progress = Math.min((value / currentMilestone.target) * 100, 100); + const isCompleted = value >= currentMilestone.target; + + // Milestone navigation + const nextMilestone = () => { + setCurrentMilestoneIndex((prev) => + prev < milestones.length - 1 ? prev + 1 : 0 + ); + }; + + const prevMilestone = () => { + setCurrentMilestoneIndex((prev) => + prev > 0 ? prev - 1 : milestones.length - 1 + ); + }; + + const handleProgressBarClick = () => { + if (onStatsToggle) { + onStatsToggle(); + } + }; + + // Animate number changes + useEffect(() => { + if (!animate) { + setDisplayValue(value); + return; + } + + const duration = 1000; // 1 second animation + const steps = 60; // 60fps + const stepValue = (value - displayValue) / steps; + + if (Math.abs(stepValue) < 0.01) { + setDisplayValue(value); + return; + } + + const timer = setInterval(() => { + setDisplayValue(prev => { + const next = prev + stepValue; + if (Math.abs(next - value) < Math.abs(stepValue)) { + clearInterval(timer); + return value; + } + return next; + }); + }, duration / steps); + + return () => clearInterval(timer); + }, [value, displayValue, animate]); + + // Format number appropriately for shares + const formatValue = (value: number) => { + // If it's a whole number, show it as integer + if (value % 1 === 0) { + return value.toString(); + } + // Otherwise show up to 6 decimal places, removing trailing zeros + return value.toFixed(6).replace(/\.?0+$/, ''); + }; + + const formattedValue = formatValue(displayValue); + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR', + minimumFractionDigits: 4, + }).format(amount); + }; + + return ( +
{ + if (hoverTimeout) { + clearTimeout(hoverTimeout); + setHoverTimeout(null); + } + setIsHovered(true); + if (onHover) onHover(true); + }} + onMouseLeave={() => { + // Delay hiding to allow moving to progress bar + const timeout = setTimeout(() => { + setIsHovered(false); + if (onHover) onHover(false); + }, 300); // 300ms delay + setHoverTimeout(timeout); + }} + > +
+ {/* Background glow effect */} +
+ {formattedValue} +
+ + {/* Main LED text */} +
+ {formattedValue} +
+ + {/* Subtle scan line effect */} +
+
+
+
+ + {/* Label */} +
+ total shares +
+ + {/* Hover overlay with price and add button */} +
+ {/* Current Price Display */} + {currentPrice ? ( +
+
+ current price +
+
+ {formatCurrency(currentPrice)} +
+
+ ) : ( +
+ no price data +
+ )} +
+ + {/* Progress Bar - shows when hovered */} +
+
+ {/* Progress Bar */} +
+ {/* Background pulse for completed milestones */} + {isCompleted && ( +
+ )} + + {/* Progress fill */} +
+ + {/* Glow effect */} +
+
+ + {/* Milestone Info */} +
+ {/* Left: Previous milestone button */} + + + {/* Center: Milestone info */} +
+
+ {value.toFixed(2)} / {currentMilestone.target} +
+ +
+ {currentMilestone.label} +
+ +
+ {isCompleted ? 'COMPLETED' : `${(100 - progress).toFixed(1)}% TO GO`} +
+
+ + {/* Right: Add Purchase, Next milestone button and stats toggle */} +
+ {/* Add Purchase Button */} + {onAddPurchase && ( + + )} + + + + {/* Stats indicator */} +
+ {showStats ? '▲' : '▼'} +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/resources/js/components/Display/MilestoneProgressBar.tsx b/resources/js/components/Display/MilestoneProgressBar.tsx new file mode 100644 index 0000000..36bbd64 --- /dev/null +++ b/resources/js/components/Display/MilestoneProgressBar.tsx @@ -0,0 +1,167 @@ +import { cn } from '@/lib/utils'; +import { ChevronLeft, ChevronRight, Plus } from 'lucide-react'; +import { useState } from 'react'; + +interface Milestone { + target: number; + label: string; + color: string; +} + +interface MilestoneProgressBarProps { + currentShares: number; + className?: string; + onStatsToggle?: () => void; + showStats?: boolean; + isVisible?: boolean; + onAddPurchase?: () => void; + onHover?: (isHovered: boolean) => void; +} + +const milestones: Milestone[] = [ + { target: 1500, label: '1.5K', color: 'bg-blue-500' }, + { target: 3000, label: '3K', color: 'bg-green-500' }, + { target: 4500, label: '4.5K', color: 'bg-yellow-500' }, + { target: 6000, label: '6K', color: 'bg-red-500' }, +]; + +export default function MilestoneProgressBar({ + currentShares, + className, + onStatsToggle, + showStats = false, + isVisible = false, + onAddPurchase, + onHover +}: MilestoneProgressBarProps) { + const [currentMilestoneIndex, setCurrentMilestoneIndex] = useState(0); + + const currentMilestone = milestones[currentMilestoneIndex]; + const progress = Math.min((currentShares / currentMilestone.target) * 100, 100); + const isCompleted = currentShares >= currentMilestone.target; + + const nextMilestone = () => { + setCurrentMilestoneIndex((prev) => + prev < milestones.length - 1 ? prev + 1 : 0 + ); + }; + + const prevMilestone = () => { + setCurrentMilestoneIndex((prev) => + prev > 0 ? prev - 1 : milestones.length - 1 + ); + }; + + const handleBarClick = () => { + if (onStatsToggle) { + onStatsToggle(); + } + }; + + return ( +
onHover?.(true)} + onMouseLeave={() => onHover?.(false)} + > +
+ {/* Progress Bar */} +
+ {/* Background pulse for completed milestones */} + {isCompleted && ( +
+ )} + + {/* Progress fill */} +
+ + {/* Glow effect */} +
+
+ + {/* Milestone Info */} +
+ {/* Left: Previous milestone button */} + + + {/* Center: Milestone info */} +
+
+ {currentShares.toFixed(2)} / {currentMilestone.target} +
+ +
+ {currentMilestone.label} +
+ +
+ {isCompleted ? 'COMPLETED' : `${(100 - progress).toFixed(1)}% TO GO`} +
+
+ + {/* Right: Add Purchase, Next milestone button and stats toggle */} +
+ {/* Add Purchase Button */} + {onAddPurchase && ( + + )} + + + + {/* Stats indicator */} +
+ {showStats ? '▲' : '▼'} +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/resources/js/components/Display/PurchaseModal.tsx b/resources/js/components/Display/PurchaseModal.tsx new file mode 100644 index 0000000..683a91c --- /dev/null +++ b/resources/js/components/Display/PurchaseModal.tsx @@ -0,0 +1,33 @@ +import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; + +interface PurchaseModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess?: () => void; +} + +export default function PurchaseModal({ isOpen, onClose, onSuccess }: PurchaseModalProps) { + const handleSuccess = () => { + if (onSuccess) { + onSuccess(); + } + onClose(); + }; + + return ( + + + + + ADD PURCHASE + + + +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/resources/js/components/Display/StatsPanel.tsx b/resources/js/components/Display/StatsPanel.tsx new file mode 100644 index 0000000..00ae6f2 --- /dev/null +++ b/resources/js/components/Display/StatsPanel.tsx @@ -0,0 +1,193 @@ +import { cn } from '@/lib/utils'; +import { useState } from 'react'; + +interface StatsData { + totalShares: number; + totalInvestment: number; + averageCostPerShare: number; + currentPrice?: number; + currentValue?: number; + profitLoss?: number; + profitLossPercentage?: number; +} + +interface StatsPanelProps { + stats: StatsData; + isVisible: boolean; + className?: string; +} + +export default function StatsPanel({ stats, isVisible, className }: StatsPanelProps) { + const [withdrawalRate, setWithdrawalRate] = useState(0.03); // 3% default + + const calculateWithdrawal = (rate: number) => { + if (!stats.currentValue) return 0; + return stats.currentValue * rate; + }; + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR', + minimumFractionDigits: 2, + }).format(amount); + }; + + const formatPercentage = (percentage: number) => { + return `${percentage >= 0 ? '+' : ''}${percentage.toFixed(2)}%`; + }; + + return ( +
+
+
+ + {/* Portfolio Overview */} +
+

+ Portfolio +

+ +
+
+ Shares: + {stats.totalShares.toFixed(6)} +
+ +
+ Invested: + {formatCurrency(stats.totalInvestment)} +
+ +
+ Avg Cost: + {formatCurrency(stats.averageCostPerShare)} +
+
+
+ + {/* Current Value */} + {stats.currentPrice && ( +
+

+ Current Value +

+ +
+
+ Price: + {formatCurrency(stats.currentPrice)} +
+ +
+ Value: + {formatCurrency(stats.currentValue || 0)} +
+ + {stats.profitLoss !== undefined && ( +
+ P&L: + = 0 ? "text-green-400" : "text-red-500" + )}> + {formatCurrency(stats.profitLoss)} + +
+ )} + + {stats.profitLossPercentage !== undefined && ( +
+ Return: + = 0 ? "text-green-400" : "text-red-500" + )}> + {formatPercentage(stats.profitLossPercentage)} + +
+ )} +
+
+ )} + + {/* Withdrawal Estimates */} + {stats.currentValue && ( +
+

+ Annual Withdrawal +

+ +
+
+ 3%: + {formatCurrency(calculateWithdrawal(0.03))} +
+ +
+ 4%: + {formatCurrency(calculateWithdrawal(0.04))} +
+ +
+ Custom: +
+ setWithdrawalRate(Number(e.target.value) / 100)} + className="w-12 bg-transparent text-red-400 text-right text-xs border-b border-red-500/30 focus:border-red-400 outline-none" + min="0" + max="10" + step="0.1" + /> + % +
+
+ +
+ + {formatCurrency(calculateWithdrawal(withdrawalRate))} +
+
+
+ )} + + {/* Monthly Breakdown */} + {stats.currentValue && ( +
+

+ Monthly Income +

+ +
+
+ 3%: + {formatCurrency(calculateWithdrawal(0.03) / 12)} +
+ +
+ 4%: + {formatCurrency(calculateWithdrawal(0.04) / 12)} +
+ +
+ Custom: + {formatCurrency(calculateWithdrawal(withdrawalRate) / 12)} +
+
+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/resources/js/components/Transactions/AddPurchaseForm.tsx b/resources/js/components/Transactions/AddPurchaseForm.tsx index 025b87a..edc42ba 100644 --- a/resources/js/components/Transactions/AddPurchaseForm.tsx +++ b/resources/js/components/Transactions/AddPurchaseForm.tsx @@ -12,9 +12,14 @@ interface PurchaseFormData { shares: string; price_per_share: string; total_cost: string; + [key: string]: string; } -export default function AddPurchaseForm() { +interface AddPurchaseFormProps { + onSuccess?: () => void; +} + +export default function AddPurchaseForm({ onSuccess }: AddPurchaseFormProps) { const { data, setData, post, processing, errors, reset } = useForm({ date: new Date().toISOString().split('T')[0], // Today's date in YYYY-MM-DD format shares: '', @@ -42,31 +47,32 @@ export default function AddPurchaseForm() { onSuccess: () => { reset(); setData('date', new Date().toISOString().split('T')[0]); + if (onSuccess) { + onSuccess(); + } }, }); }; return ( - - - Add VWCE Purchase - - +
+
- + setData('date', e.target.value)} max={new Date().toISOString().split('T')[0]} + className="bg-black border-red-500/30 text-red-400 focus:border-red-400" />
- + setData('shares', e.target.value)} + className="bg-black border-red-500/30 text-red-400 focus:border-red-400 placeholder:text-red-400/30" />
- + setData('price_per_share', e.target.value)} + className="bg-black border-red-500/30 text-red-400 focus:border-red-400 placeholder:text-red-400/30" />
- + setData('total_cost', e.target.value)} - className="bg-neutral-50 dark:bg-neutral-800" + className="bg-black border-red-500/30 text-red-400 focus:border-red-400 placeholder:text-red-400/30" /> -

+

Auto-calculated from shares × price

@@ -114,13 +122,13 @@ export default function AddPurchaseForm() { - - +
+
); } \ No newline at end of file diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index 822c3dc..0871ac6 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -1,35 +1,140 @@ -import { PlaceholderPattern } from '@/components/ui/PlaceholderPattern'; -import AppLayout from '@/layouts/app-layout'; -import { type BreadcrumbItem } from '@/types'; +import LedCounter from '@/components/Display/LedCounter'; +import PurchaseModal from '@/components/Display/PurchaseModal'; +import StatsPanel from '@/components/Display/StatsPanel'; import { Head } from '@inertiajs/react'; +import { useEffect, useState } from 'react'; -const breadcrumbs: BreadcrumbItem[] = [ - { - title: 'Dashboard', - href: '/dashboard', - }, -]; +interface PurchaseSummary { + total_shares: number; + total_investment: number; + average_cost_per_share: number; +} + +interface CurrentPrice { + current_price: number | null; +} export default function Dashboard() { + const [purchaseData, setPurchaseData] = useState({ + total_shares: 0, + total_investment: 0, + average_cost_per_share: 0, + }); + + const [priceData, setPriceData] = useState({ + current_price: null, + }); + + const [showStats, setShowStats] = useState(false); + const [showPurchaseModal, setShowPurchaseModal] = useState(false); + const [loading, setLoading] = useState(true); + + // Fetch purchase summary and current price + useEffect(() => { + const fetchData = async () => { + try { + const [purchaseResponse, priceResponse] = await Promise.all([ + fetch('/purchases/summary'), + fetch('/pricing/current'), + ]); + + if (purchaseResponse.ok) { + const purchases = await purchaseResponse.json(); + setPurchaseData(purchases); + } + + if (priceResponse.ok) { + const price = await priceResponse.json(); + setPriceData(price); + } + } catch (error) { + console.error('Failed to fetch data:', error); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + // Refresh data after successful purchase + const handlePurchaseSuccess = async () => { + try { + const purchaseResponse = await fetch('/purchases/summary'); + if (purchaseResponse.ok) { + const purchases = await purchaseResponse.json(); + setPurchaseData(purchases); + } + } catch (error) { + console.error('Failed to refresh purchase data:', error); + } + }; + + + // Calculate portfolio stats + const currentValue = priceData.current_price + ? purchaseData.total_shares * priceData.current_price + : undefined; + + const profitLoss = currentValue + ? currentValue - purchaseData.total_investment + : undefined; + + const profitLossPercentage = profitLoss && purchaseData.total_investment > 0 + ? (profitLoss / purchaseData.total_investment) * 100 + : undefined; + + const statsData = { + totalShares: purchaseData.total_shares, + totalInvestment: purchaseData.total_investment, + averageCostPerShare: purchaseData.average_cost_per_share, + currentPrice: priceData.current_price || undefined, + currentValue, + profitLoss, + profitLossPercentage, + }; + + if (loading) { + return ( + <> + +
+
+ LOADING... +
+
+ + ); + } + return ( - - -
-
-
- -
-
- -
-
- -
-
-
- -
+ <> + + + {/* Main LED Display */} +
+ setShowStats(!showStats)} + showStats={showStats} + onAddPurchase={() => setShowPurchaseModal(true)} + /> + + {/* Stats Panel */} + + + {/* Purchase Modal */} + setShowPurchaseModal(false)} + onSuccess={handlePurchaseSuccess} + />
- + ); } diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 8218267..018eab2 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -37,7 +37,7 @@ - + @routes @viteReactRefresh diff --git a/routes/web.php b/routes/web.php index 59d6907..ae50c44 100644 --- a/routes/web.php +++ b/routes/web.php @@ -6,7 +6,7 @@ use Inertia\Inertia; Route::get('/', function () { - return Inertia::render('welcome'); + return redirect('/dashboard'); })->name('home'); Route::get('dashboard', function () { From 3c00e5732f50f898ca59d82a4a61d5a28790ad09 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 12 Jul 2025 18:09:11 +0200 Subject: [PATCH 05/15] Basic milestones --- .../Milestones/MilestoneController.php | 42 ++++++++++ .../js/components/Display/LedCounter.tsx | 18 +++- .../js/components/Display/MilestoneModal.tsx | 33 ++++++++ .../Milestones/AddMilestoneForm.tsx | 82 +++++++++++++++++++ resources/js/pages/dashboard.tsx | 13 +++ routes/web.php | 7 ++ 6 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 app/Http/Controllers/Milestones/MilestoneController.php create mode 100644 resources/js/components/Display/MilestoneModal.tsx create mode 100644 resources/js/components/Milestones/AddMilestoneForm.tsx diff --git a/app/Http/Controllers/Milestones/MilestoneController.php b/app/Http/Controllers/Milestones/MilestoneController.php new file mode 100644 index 0000000..aee0ef7 --- /dev/null +++ b/app/Http/Controllers/Milestones/MilestoneController.php @@ -0,0 +1,42 @@ +validate([ + 'target' => 'required|integer|min:1', + 'description' => 'required|string|max:255', + ]); + + // For now, just return success without persisting to database + // This allows the frontend form to work properly + return response()->json([ + 'message' => 'Milestone created successfully', + 'milestone' => [ + 'target' => $request->target, + 'description' => $request->description, + 'created_at' => now(), + ] + ], 201); + } + + /** + * Get all milestones. + */ + public function index(): JsonResponse + { + // For now, return empty array + // Later this could fetch from database + return response()->json([]); + } +} \ No newline at end of file diff --git a/resources/js/components/Display/LedCounter.tsx b/resources/js/components/Display/LedCounter.tsx index 85ed4b3..b79b497 100644 --- a/resources/js/components/Display/LedCounter.tsx +++ b/resources/js/components/Display/LedCounter.tsx @@ -12,6 +12,7 @@ interface LedCounterProps { onStatsToggle?: () => void; showStats?: boolean; onAddPurchase?: () => void; + onAddMilestone?: () => void; } export default function LedCounter({ @@ -22,7 +23,8 @@ export default function LedCounter({ onHover, onStatsToggle, showStats = false, - onAddPurchase + onAddPurchase, + onAddMilestone }: LedCounterProps) { const [displayValue, setDisplayValue] = useState(0); const [isHovered, setIsHovered] = useState(false); @@ -260,7 +262,7 @@ export default function LedCounter({
- {/* Right: Add Purchase, Next milestone button and stats toggle */} + {/* Right: Add Purchase, Add Milestone, Next milestone button and stats toggle */}
{/* Add Purchase Button */} {onAddPurchase && ( @@ -273,6 +275,18 @@ export default function LedCounter({ ADD )} + + {/* Add Milestone Button */} + {onAddMilestone && ( + + )} + +
+
+ ); +} \ No newline at end of file diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index 0871ac6..e9a97e6 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -1,4 +1,5 @@ import LedCounter from '@/components/Display/LedCounter'; +import MilestoneModal from '@/components/Display/MilestoneModal'; import PurchaseModal from '@/components/Display/PurchaseModal'; import StatsPanel from '@/components/Display/StatsPanel'; import { Head } from '@inertiajs/react'; @@ -27,6 +28,7 @@ export default function Dashboard() { const [showStats, setShowStats] = useState(false); const [showPurchaseModal, setShowPurchaseModal] = useState(false); + const [showMilestoneModal, setShowMilestoneModal] = useState(false); const [loading, setLoading] = useState(true); // Fetch purchase summary and current price @@ -120,6 +122,7 @@ export default function Dashboard() { onStatsToggle={() => setShowStats(!showStats)} showStats={showStats} onAddPurchase={() => setShowPurchaseModal(true)} + onAddMilestone={() => setShowMilestoneModal(true)} /> {/* Stats Panel */} @@ -134,6 +137,16 @@ export default function Dashboard() { onClose={() => setShowPurchaseModal(false)} onSuccess={handlePurchaseSuccess} /> + + {/* Milestone Modal */} + setShowMilestoneModal(false)} + onSuccess={() => { + // Could refresh milestone data here if needed + console.log('Milestone added successfully'); + }} + />
); diff --git a/routes/web.php b/routes/web.php index ae50c44..3ee6392 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,6 +2,7 @@ use App\Http\Controllers\Transactions\PurchaseController; use App\Http\Controllers\Pricing\PricingController; +use App\Http\Controllers\Milestones\MilestoneController; use Illuminate\Support\Facades\Route; use Inertia\Inertia; @@ -29,5 +30,11 @@ Route::get('/date/{date}', [PricingController::class, 'forDate'])->name('for-date'); }); +// Milestone routes +Route::prefix('milestones')->name('milestones.')->group(function () { + Route::get('/', [MilestoneController::class, 'index'])->name('index'); + Route::post('/', [MilestoneController::class, 'store'])->name('store'); +}); + require __DIR__.'/settings.php'; require __DIR__.'/auth.php'; From 3586b1b039612273568f4a35f9fda07f16963d83 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 12 Jul 2025 19:59:22 +0200 Subject: [PATCH 06/15] Re-arrange components --- .../js/components/Display/InlineForm.tsx | 68 ++++++++ .../js/components/Display/LedDisplay.tsx | 104 ++++++++++++ .../js/components/Display/ProgressBar.tsx | 60 +++++++ resources/js/components/Display/StatsBox.tsx | 156 ++++++++++++++++++ resources/js/pages/dashboard.tsx | 100 ++++++----- 5 files changed, 447 insertions(+), 41 deletions(-) create mode 100644 resources/js/components/Display/InlineForm.tsx create mode 100644 resources/js/components/Display/LedDisplay.tsx create mode 100644 resources/js/components/Display/ProgressBar.tsx create mode 100644 resources/js/components/Display/StatsBox.tsx diff --git a/resources/js/components/Display/InlineForm.tsx b/resources/js/components/Display/InlineForm.tsx new file mode 100644 index 0000000..949de4d --- /dev/null +++ b/resources/js/components/Display/InlineForm.tsx @@ -0,0 +1,68 @@ +import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm'; +import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm'; +import { cn } from '@/lib/utils'; +import { X } from 'lucide-react'; + +interface InlineFormProps { + type: 'purchase' | 'milestone' | null; + onClose: () => void; + onPurchaseSuccess?: () => void; + onMilestoneSuccess?: () => void; + className?: string; +} + +export default function InlineForm({ + type, + onClose, + onPurchaseSuccess, + onMilestoneSuccess, + className +}: InlineFormProps) { + if (!type) return null; + + const title = type === 'purchase' ? 'ADD PURCHASE' : 'ADD MILESTONE'; + + return ( +
+ {/* Header */} +
+

+ {title} +

+ +
+ + {/* Form Content */} +
+ {type === 'purchase' ? ( + { + if (onPurchaseSuccess) onPurchaseSuccess(); + onClose(); + }} + /> + ) : ( + { + if (onMilestoneSuccess) onMilestoneSuccess(); + onClose(); + }} + /> + )} +
+
+ ); +} \ No newline at end of file diff --git a/resources/js/components/Display/LedDisplay.tsx b/resources/js/components/Display/LedDisplay.tsx new file mode 100644 index 0000000..c12f0c6 --- /dev/null +++ b/resources/js/components/Display/LedDisplay.tsx @@ -0,0 +1,104 @@ +import { cn } from '@/lib/utils'; +import { useEffect, useState } from 'react'; + +interface LedDisplayProps { + value: number; + className?: string; + animate?: boolean; + onClick?: () => void; +} + +export default function LedDisplay({ + value, + className, + animate = true, + onClick +}: LedDisplayProps) { + const [displayValue, setDisplayValue] = useState(0); + + // Animate number changes + useEffect(() => { + if (!animate) { + setDisplayValue(value); + return; + } + + const duration = 1000; // 1 second animation + const steps = 60; // 60fps + const stepValue = (value - displayValue) / steps; + + if (Math.abs(stepValue) < 0.01) { + setDisplayValue(value); + return; + } + + const timer = setInterval(() => { + setDisplayValue(prev => { + const next = prev + stepValue; + if (Math.abs(next - value) < Math.abs(stepValue)) { + clearInterval(timer); + return value; + } + return next; + }); + }, duration / steps); + + return () => clearInterval(timer); + }, [value, displayValue, animate]); + + // Format number appropriately for shares + const formatValue = (value: number) => { + // If it's a whole number, show it as integer + if (value % 1 === 0) { + return value.toString(); + } + // Otherwise show up to 6 decimal places, removing trailing zeros + return value.toFixed(6).replace(/\.?0+$/, ''); + }; + + const formattedValue = formatValue(displayValue); + + return ( +
+
+ {/* Background glow effect */} +
+ {formattedValue} +
+ + {/* Main LED text */} +
+ {formattedValue} +
+ + {/* Subtle scan line effect */} +
+
+
+
+ + {/* Label */} +
+ total shares +
+
+ ); +} \ No newline at end of file diff --git a/resources/js/components/Display/ProgressBar.tsx b/resources/js/components/Display/ProgressBar.tsx new file mode 100644 index 0000000..a777d3a --- /dev/null +++ b/resources/js/components/Display/ProgressBar.tsx @@ -0,0 +1,60 @@ +import { cn } from '@/lib/utils'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { useState } from 'react'; + +interface ProgressBarProps { + value: number; + className?: string; + onClick?: () => void; +} + +export default function ProgressBar({ + value, + className, + onClick +}: ProgressBarProps) { + const [currentMilestoneIndex, setCurrentMilestoneIndex] = useState(0); + + // Milestone definitions + const milestones = [ + { target: 1500, label: '1.5K', color: 'bg-blue-500' }, + { target: 3000, label: '3K', color: 'bg-green-500' }, + { target: 4500, label: '4.5K', color: 'bg-yellow-500' }, + { target: 6000, label: '6K', color: 'bg-red-500' }, + ]; + + const currentMilestone = milestones[currentMilestoneIndex]; + const progress = Math.min((value / currentMilestone.target) * 100, 100); + const isCompleted = value >= currentMilestone.target; + + // Milestone navigation + const nextMilestone = () => { + setCurrentMilestoneIndex((prev) => + prev < milestones.length - 1 ? prev + 1 : 0 + ); + }; + + const prevMilestone = () => { + setCurrentMilestoneIndex((prev) => + prev > 0 ? prev - 1 : milestones.length - 1 + ); + }; + + return ( +
+
+ PROGRESS BAR +
+
+ ); +} \ No newline at end of file diff --git a/resources/js/components/Display/StatsBox.tsx b/resources/js/components/Display/StatsBox.tsx new file mode 100644 index 0000000..c0e426e --- /dev/null +++ b/resources/js/components/Display/StatsBox.tsx @@ -0,0 +1,156 @@ +import { cn } from '@/lib/utils'; +import { Plus } from 'lucide-react'; + +interface StatsBoxProps { + stats: { + totalShares: number; + totalInvestment: number; + averageCostPerShare: number; + currentPrice?: number; + currentValue?: number; + profitLoss?: number; + profitLossPercentage?: number; + }; + className?: string; + onAddPurchase?: () => void; + onAddMilestone?: () => void; +} + +export default function StatsBox({ + stats, + className, + onAddPurchase, + onAddMilestone +}: StatsBoxProps) { + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(amount); + }; + + const formatCurrencyDetailed = (amount: number) => { + return new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR', + minimumFractionDigits: 4, + }).format(amount); + }; + + return ( +
+ {/* Current Price */} + {stats.currentPrice && ( +
+
+ current price +
+
+ {formatCurrencyDetailed(stats.currentPrice)} +
+
+ )} + + {/* Portfolio Stats Grid */} +
+ {/* Total Investment */} +
+
Total Investment
+
+ {formatCurrency(stats.totalInvestment)} +
+
+ + {/* Current Value */} + {stats.currentValue && ( +
+
Current Value
+
+ {formatCurrency(stats.currentValue)} +
+
+ )} + + {/* Average Cost */} +
+
Avg Cost/Share
+
+ {formatCurrencyDetailed(stats.averageCostPerShare)} +
+
+ + {/* Profit/Loss */} + {stats.profitLoss !== undefined && ( +
+
P&L
+
= 0 ? "text-green-400" : "text-red-400" + )}> + {stats.profitLoss >= 0 ? '+' : ''}{formatCurrency(stats.profitLoss)} +
+
+ )} +
+ + {/* Withdrawal Estimates */} + {stats.currentValue && ( +
+
Annual Withdrawal (Safe)
+
+
+
3% Rule
+
+ {formatCurrency(stats.currentValue * 0.03)} +
+
+
+
4% Rule
+
+ {formatCurrency(stats.currentValue * 0.04)} +
+
+
+
+ )} + + {/* Action Buttons */} +
+
+ {/* Add Purchase Button */} + {onAddPurchase && ( + + )} + + {/* Add Milestone Button */} + {onAddMilestone && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index e9a97e6..c9c3c1d 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -1,7 +1,7 @@ -import LedCounter from '@/components/Display/LedCounter'; -import MilestoneModal from '@/components/Display/MilestoneModal'; -import PurchaseModal from '@/components/Display/PurchaseModal'; -import StatsPanel from '@/components/Display/StatsPanel'; +import LedDisplay from '@/components/Display/LedDisplay'; +import InlineForm from '@/components/Display/InlineForm'; +import ProgressBar from '@/components/Display/ProgressBar'; +import StatsBox from '@/components/Display/StatsBox'; import { Head } from '@inertiajs/react'; import { useEffect, useState } from 'react'; @@ -26,9 +26,9 @@ export default function Dashboard() { current_price: null, }); - const [showStats, setShowStats] = useState(false); - const [showPurchaseModal, setShowPurchaseModal] = useState(false); - const [showMilestoneModal, setShowMilestoneModal] = useState(false); + const [showProgressBar, setShowProgressBar] = useState(false); + const [showStatsBox, setShowStatsBox] = useState(false); + const [activeForm, setActiveForm] = useState<'purchase' | 'milestone' | null>(null); const [loading, setLoading] = useState(true); // Fetch purchase summary and current price @@ -109,44 +109,62 @@ export default function Dashboard() { ); } + // Toggle handlers with cascading behavior + const handleLedClick = () => { + const newShowProgressBar = !showProgressBar; + setShowProgressBar(newShowProgressBar); + if (!newShowProgressBar) { + // If hiding progress bar, also hide stats box + setShowStatsBox(false); + } + }; + + const handleProgressClick = () => { + setShowStatsBox(!showStatsBox); + }; + return ( <> - {/* Main LED Display */} -
- setShowStats(!showStats)} - showStats={showStats} - onAddPurchase={() => setShowPurchaseModal(true)} - onAddMilestone={() => setShowMilestoneModal(true)} - /> - - {/* Stats Panel */} - - - {/* Purchase Modal */} - setShowPurchaseModal(false)} - onSuccess={handlePurchaseSuccess} - /> - - {/* Milestone Modal */} - setShowMilestoneModal(false)} - onSuccess={() => { - // Could refresh milestone data here if needed - console.log('Milestone added successfully'); - }} - /> + {/* Stacked Layout */} +
+
+ {/* Box 1: LED Number Display */} + + + {/* Box 2: Progress Bar (toggleable) */} +
+ +
+ + {/* Box 3: Stats Box (toggleable) */} +
+ setActiveForm('purchase')} + onAddMilestone={() => setActiveForm('milestone')} + /> +
+ + {/* Box 4: Forms (only when active form is set) */} +
+ setActiveForm(null)} + onPurchaseSuccess={handlePurchaseSuccess} + onMilestoneSuccess={() => { + console.log('Milestone added successfully'); + }} + /> +
+
); From ce6b6e00c5a607762289c6a63572cb7f2d495236 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 12 Jul 2025 21:55:55 +0200 Subject: [PATCH 07/15] Add number font --- public/fonts/7segment.woff | Bin 0 -> 1952 bytes resources/css/app.css | 13 +++++- .../js/components/Display/LedCounter.tsx | 42 +++++++++--------- .../js/components/Display/LedDisplay.tsx | 4 +- .../components/Settings/UserMenuContent.tsx | 2 +- resources/views/app.blade.php | 2 + 6 files changed, 38 insertions(+), 25 deletions(-) create mode 100644 public/fonts/7segment.woff diff --git a/public/fonts/7segment.woff b/public/fonts/7segment.woff new file mode 100644 index 0000000000000000000000000000000000000000..09ab3150d27946e0bf9697bc3badd40827c7d5ae GIT binary patch literal 1952 zcmZuydpy+J7T>>z5sgQV@_ytsp2rB24vFD73fCiwaLpLQkjEH@4NT@uD#aY>$le0pPlOM<^~V| zo3sGPOGd?NN&o+v_fgAz0ARP_8X4l4OdmSVh@-OsKz{%LA;NJU=pI>Oq;PcsAk0G@ zRS7T?;1bD>ii2n1kn2ex>X-mr zk|9n4-murN$afeW@F%0mj?BmoRNh zOUjw|b1NI)q-YTys;LrEgIh#PO=k|ZYousOt;)%a?Mu{ZjuXg^B~6H0{P%JyR;v9r z{DOZu*wwtT@^!#r%^MH9{OoN36Zh!f^ntdu=FY(=x`j|fPhP2TIJrRI)g3-iIFg_FTpTsb(06_LX4TrG+ z=Pig?M51_cpChd*(mW!M{(;fMWYdjlJyNyF=N(jZ`kLEOd7S=F#eKenf>9lD!rXY^-%(MpBFE};<5fT7j3QN)-#Flj>O_@V1xeX%E# z$t1ESUj_reIS`s)K47MRGVBpR*v<|LP{7-kQo#;5Cj;qih5!t%%R=@imssruw!j0> z0T)~bWuO+k0R3PVY$9^VUc?S@Lug1UQh<~pb;vX16*7-(p}SBWl#IHffoLq6juxP$ zXdT*)4xsZGz~nJK>;QHI3&divbSxJu!|r2^*b7XAO<~J8jw|5mxEW5topDe6IL^XZ zlHBJPBM}cdUN8^I91Aqt?Hdt+5E;_`9V|>S7AR7jYAVS& zllL)I;BEyX1OU|Rsr@%|rEYaebBVp)B3>1r8$Tn1e`23soU(N=7C0x*R3g{@ZoDom`+FhQUz$Dhz30k))$AnoG=4_&l8M0ddGVHta&OSt=C8u8Jxe>~ z!Jo~`;_82KqHYNqoN`PngEekZb#5M9rTltnYWZ+~W3Cc^R!-Tgsx2a*rj_Pm7?3$; zo4SAVBB|f=*6WH{JVV~;U__JSx^_TcdeszX#-sIUQ$fYN#X&-OmTH*Tgpi}19drLC z#dYo7fPO;7>9y7tt?{{G*1r^whc_AM^o~Rw4(Q#o*&G}hRi3ok^1StADMdrK)bCnH zk$zym{OS88+>d>4W}KG`9gPlO7$Vy8;~fM39q*@^U90M zC|6FBzt2{AnR7Zme$j|zTV9>PHU8rnw^F08!E`L~je1FQCz#ER)HCmYPg$#%-F&mteKqBy!A96(o19>B%ivYtgL@s; zv!^Ny+ZW{a#LVAo<907`ItAK)q@8^m!p(2fVybVzQl|mA?fbCZiPvD&>x)NffH`=K zeK|C-)6t>^F|Pq;HOLm(%Ay99%pKuOQj=PpQoCH76|tUfmY#KD@4|-h#p0t^kMio* zZjFh3ys`cRi%*w3Pgz;2Z(;d1tkoNzaw;YSqwzidNsHf(Hk4i;^c$fl?9Y80H15Tt pUeq6{?^!qzt(GUdw;OsRx#N+vH8JV#yci)!5#VE&W{(TOe*s8D?%MzW literal 0 HcmV?d00001 diff --git a/resources/css/app.css b/resources/css/app.css index 2b360a8..912a683 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -5,12 +5,23 @@ @source '../views'; @source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; +@font-face { + font-family: '7Segment'; + src: url('/fonts/7segment.woff') format('woff'); + font-weight: normal; + font-style: normal; +} + +.font-digital { + font-family: '7Segment', monospace; +} + @custom-variant dark (&:is(.dark *)); @theme { --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - + --font-mono-display: 'Major Mono Display', ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace; diff --git a/resources/js/components/Display/LedCounter.tsx b/resources/js/components/Display/LedCounter.tsx index b79b497..f5c72f2 100644 --- a/resources/js/components/Display/LedCounter.tsx +++ b/resources/js/components/Display/LedCounter.tsx @@ -15,9 +15,9 @@ interface LedCounterProps { onAddMilestone?: () => void; } -export default function LedCounter({ - value, - className, +export default function LedCounter({ + value, + className, animate = true, currentPrice, onHover, @@ -45,13 +45,13 @@ export default function LedCounter({ // Milestone navigation const nextMilestone = () => { - setCurrentMilestoneIndex((prev) => + setCurrentMilestoneIndex((prev) => prev < milestones.length - 1 ? prev + 1 : 0 ); }; const prevMilestone = () => { - setCurrentMilestoneIndex((prev) => + setCurrentMilestoneIndex((prev) => prev > 0 ? prev - 1 : milestones.length - 1 ); }; @@ -72,7 +72,7 @@ export default function LedCounter({ const duration = 1000; // 1 second animation const steps = 60; // 60fps const stepValue = (value - displayValue) / steps; - + if (Math.abs(stepValue) < 0.01) { setDisplayValue(value); return; @@ -101,7 +101,7 @@ export default function LedCounter({ // Otherwise show up to 6 decimal places, removing trailing zeros return value.toFixed(6).replace(/\.?0+$/, ''); }; - + const formattedValue = formatValue(displayValue); const formatCurrency = (amount: number) => { @@ -113,7 +113,7 @@ export default function LedCounter({ }; return ( -
{/* Background glow effect */} -
+
{formattedValue}
- + {/* Main LED text */}
{/* Progress Bar */} -
@@ -213,9 +213,9 @@ export default function LedCounter({ {isCompleted && (
)} - + {/* Progress fill */} -
- + {/* Glow effect */} -
{value.toFixed(2)} / {currentMilestone.target}
- +
{currentMilestone.label}
- +
{isCompleted ? 'COMPLETED' : `${(100 - progress).toFixed(1)}% TO GO`}
@@ -287,7 +287,7 @@ export default function LedCounter({ MILE )} - + - + {/* Stats indicator */}
); -} \ No newline at end of file +} diff --git a/resources/js/components/Display/LedDisplay.tsx b/resources/js/components/Display/LedDisplay.tsx index c12f0c6..822242f 100644 --- a/resources/js/components/Display/LedDisplay.tsx +++ b/resources/js/components/Display/LedDisplay.tsx @@ -73,7 +73,7 @@ export default function LedDisplay({ >
{/* Background glow effect */} -
+
{formattedValue}
@@ -81,7 +81,7 @@ export default function LedDisplay({
+ + @routes @viteReactRefresh From dce75711097dfc64849844593726eb572f3df713 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 12 Jul 2025 21:59:31 +0200 Subject: [PATCH 08/15] Clean up old files --- .../js/components/Display/LedCounter.tsx | 312 ------------------ 1 file changed, 312 deletions(-) delete mode 100644 resources/js/components/Display/LedCounter.tsx diff --git a/resources/js/components/Display/LedCounter.tsx b/resources/js/components/Display/LedCounter.tsx deleted file mode 100644 index f5c72f2..0000000 --- a/resources/js/components/Display/LedCounter.tsx +++ /dev/null @@ -1,312 +0,0 @@ -import { cn } from '@/lib/utils'; -import { ChevronLeft, ChevronRight, Plus } from 'lucide-react'; -import { useEffect, useState } from 'react'; - -interface LedCounterProps { - value: number; - className?: string; - animate?: boolean; - currentPrice?: number; - onHover?: (isHovered: boolean) => void; - // Progress bar props - onStatsToggle?: () => void; - showStats?: boolean; - onAddPurchase?: () => void; - onAddMilestone?: () => void; -} - -export default function LedCounter({ - value, - className, - animate = true, - currentPrice, - onHover, - onStatsToggle, - showStats = false, - onAddPurchase, - onAddMilestone -}: LedCounterProps) { - const [displayValue, setDisplayValue] = useState(0); - const [isHovered, setIsHovered] = useState(false); - const [hoverTimeout, setHoverTimeout] = useState(null); - const [currentMilestoneIndex, setCurrentMilestoneIndex] = useState(0); - - // Milestone definitions - const milestones = [ - { target: 1500, label: '1.5K', color: 'bg-blue-500' }, - { target: 3000, label: '3K', color: 'bg-green-500' }, - { target: 4500, label: '4.5K', color: 'bg-yellow-500' }, - { target: 6000, label: '6K', color: 'bg-red-500' }, - ]; - - const currentMilestone = milestones[currentMilestoneIndex]; - const progress = Math.min((value / currentMilestone.target) * 100, 100); - const isCompleted = value >= currentMilestone.target; - - // Milestone navigation - const nextMilestone = () => { - setCurrentMilestoneIndex((prev) => - prev < milestones.length - 1 ? prev + 1 : 0 - ); - }; - - const prevMilestone = () => { - setCurrentMilestoneIndex((prev) => - prev > 0 ? prev - 1 : milestones.length - 1 - ); - }; - - const handleProgressBarClick = () => { - if (onStatsToggle) { - onStatsToggle(); - } - }; - - // Animate number changes - useEffect(() => { - if (!animate) { - setDisplayValue(value); - return; - } - - const duration = 1000; // 1 second animation - const steps = 60; // 60fps - const stepValue = (value - displayValue) / steps; - - if (Math.abs(stepValue) < 0.01) { - setDisplayValue(value); - return; - } - - const timer = setInterval(() => { - setDisplayValue(prev => { - const next = prev + stepValue; - if (Math.abs(next - value) < Math.abs(stepValue)) { - clearInterval(timer); - return value; - } - return next; - }); - }, duration / steps); - - return () => clearInterval(timer); - }, [value, displayValue, animate]); - - // Format number appropriately for shares - const formatValue = (value: number) => { - // If it's a whole number, show it as integer - if (value % 1 === 0) { - return value.toString(); - } - // Otherwise show up to 6 decimal places, removing trailing zeros - return value.toFixed(6).replace(/\.?0+$/, ''); - }; - - const formattedValue = formatValue(displayValue); - - const formatCurrency = (amount: number) => { - return new Intl.NumberFormat('de-DE', { - style: 'currency', - currency: 'EUR', - minimumFractionDigits: 4, - }).format(amount); - }; - - return ( -
{ - if (hoverTimeout) { - clearTimeout(hoverTimeout); - setHoverTimeout(null); - } - setIsHovered(true); - if (onHover) onHover(true); - }} - onMouseLeave={() => { - // Delay hiding to allow moving to progress bar - const timeout = setTimeout(() => { - setIsHovered(false); - if (onHover) onHover(false); - }, 300); // 300ms delay - setHoverTimeout(timeout); - }} - > -
- {/* Background glow effect */} -
- {formattedValue} -
- - {/* Main LED text */} -
- {formattedValue} -
- - {/* Subtle scan line effect */} -
-
-
-
- - {/* Label */} -
- total shares -
- - {/* Hover overlay with price and add button */} -
- {/* Current Price Display */} - {currentPrice ? ( -
-
- current price -
-
- {formatCurrency(currentPrice)} -
-
- ) : ( -
- no price data -
- )} -
- - {/* Progress Bar - shows when hovered */} -
-
- {/* Progress Bar */} -
- {/* Background pulse for completed milestones */} - {isCompleted && ( -
- )} - - {/* Progress fill */} -
- - {/* Glow effect */} -
-
- - {/* Milestone Info */} -
- {/* Left: Previous milestone button */} - - - {/* Center: Milestone info */} -
-
- {value.toFixed(2)} / {currentMilestone.target} -
- -
- {currentMilestone.label} -
- -
- {isCompleted ? 'COMPLETED' : `${(100 - progress).toFixed(1)}% TO GO`} -
-
- - {/* Right: Add Purchase, Add Milestone, Next milestone button and stats toggle */} -
- {/* Add Purchase Button */} - {onAddPurchase && ( - - )} - - {/* Add Milestone Button */} - {onAddMilestone && ( - - )} - - - - {/* Stats indicator */} -
- {showStats ? '▲' : '▼'} -
-
-
-
-
-
- ); -} From 896ad1532105db71452f4be54ad5b0c4c650056a Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 12 Jul 2025 23:39:34 +0200 Subject: [PATCH 09/15] Clean up old files --- .../js/components/Display/MilestoneModal.tsx | 33 --- .../Display/MilestoneProgressBar.tsx | 167 --------------- .../js/components/Display/PurchaseModal.tsx | 33 --- .../js/components/Display/StatsPanel.tsx | 193 ------------------ 4 files changed, 426 deletions(-) delete mode 100644 resources/js/components/Display/MilestoneModal.tsx delete mode 100644 resources/js/components/Display/MilestoneProgressBar.tsx delete mode 100644 resources/js/components/Display/PurchaseModal.tsx delete mode 100644 resources/js/components/Display/StatsPanel.tsx diff --git a/resources/js/components/Display/MilestoneModal.tsx b/resources/js/components/Display/MilestoneModal.tsx deleted file mode 100644 index 848195a..0000000 --- a/resources/js/components/Display/MilestoneModal.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm'; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; - -interface MilestoneModalProps { - isOpen: boolean; - onClose: () => void; - onSuccess?: () => void; -} - -export default function MilestoneModal({ isOpen, onClose, onSuccess }: MilestoneModalProps) { - const handleSuccess = () => { - if (onSuccess) { - onSuccess(); - } - onClose(); - }; - - return ( - - - - - ADD MILESTONE - - - -
- -
-
-
- ); -} \ No newline at end of file diff --git a/resources/js/components/Display/MilestoneProgressBar.tsx b/resources/js/components/Display/MilestoneProgressBar.tsx deleted file mode 100644 index 36bbd64..0000000 --- a/resources/js/components/Display/MilestoneProgressBar.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { cn } from '@/lib/utils'; -import { ChevronLeft, ChevronRight, Plus } from 'lucide-react'; -import { useState } from 'react'; - -interface Milestone { - target: number; - label: string; - color: string; -} - -interface MilestoneProgressBarProps { - currentShares: number; - className?: string; - onStatsToggle?: () => void; - showStats?: boolean; - isVisible?: boolean; - onAddPurchase?: () => void; - onHover?: (isHovered: boolean) => void; -} - -const milestones: Milestone[] = [ - { target: 1500, label: '1.5K', color: 'bg-blue-500' }, - { target: 3000, label: '3K', color: 'bg-green-500' }, - { target: 4500, label: '4.5K', color: 'bg-yellow-500' }, - { target: 6000, label: '6K', color: 'bg-red-500' }, -]; - -export default function MilestoneProgressBar({ - currentShares, - className, - onStatsToggle, - showStats = false, - isVisible = false, - onAddPurchase, - onHover -}: MilestoneProgressBarProps) { - const [currentMilestoneIndex, setCurrentMilestoneIndex] = useState(0); - - const currentMilestone = milestones[currentMilestoneIndex]; - const progress = Math.min((currentShares / currentMilestone.target) * 100, 100); - const isCompleted = currentShares >= currentMilestone.target; - - const nextMilestone = () => { - setCurrentMilestoneIndex((prev) => - prev < milestones.length - 1 ? prev + 1 : 0 - ); - }; - - const prevMilestone = () => { - setCurrentMilestoneIndex((prev) => - prev > 0 ? prev - 1 : milestones.length - 1 - ); - }; - - const handleBarClick = () => { - if (onStatsToggle) { - onStatsToggle(); - } - }; - - return ( -
onHover?.(true)} - onMouseLeave={() => onHover?.(false)} - > -
- {/* Progress Bar */} -
- {/* Background pulse for completed milestones */} - {isCompleted && ( -
- )} - - {/* Progress fill */} -
- - {/* Glow effect */} -
-
- - {/* Milestone Info */} -
- {/* Left: Previous milestone button */} - - - {/* Center: Milestone info */} -
-
- {currentShares.toFixed(2)} / {currentMilestone.target} -
- -
- {currentMilestone.label} -
- -
- {isCompleted ? 'COMPLETED' : `${(100 - progress).toFixed(1)}% TO GO`} -
-
- - {/* Right: Add Purchase, Next milestone button and stats toggle */} -
- {/* Add Purchase Button */} - {onAddPurchase && ( - - )} - - - - {/* Stats indicator */} -
- {showStats ? '▲' : '▼'} -
-
-
-
-
- ); -} \ No newline at end of file diff --git a/resources/js/components/Display/PurchaseModal.tsx b/resources/js/components/Display/PurchaseModal.tsx deleted file mode 100644 index 683a91c..0000000 --- a/resources/js/components/Display/PurchaseModal.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm'; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; - -interface PurchaseModalProps { - isOpen: boolean; - onClose: () => void; - onSuccess?: () => void; -} - -export default function PurchaseModal({ isOpen, onClose, onSuccess }: PurchaseModalProps) { - const handleSuccess = () => { - if (onSuccess) { - onSuccess(); - } - onClose(); - }; - - return ( - - - - - ADD PURCHASE - - - -
- -
-
-
- ); -} \ No newline at end of file diff --git a/resources/js/components/Display/StatsPanel.tsx b/resources/js/components/Display/StatsPanel.tsx deleted file mode 100644 index 00ae6f2..0000000 --- a/resources/js/components/Display/StatsPanel.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { cn } from '@/lib/utils'; -import { useState } from 'react'; - -interface StatsData { - totalShares: number; - totalInvestment: number; - averageCostPerShare: number; - currentPrice?: number; - currentValue?: number; - profitLoss?: number; - profitLossPercentage?: number; -} - -interface StatsPanelProps { - stats: StatsData; - isVisible: boolean; - className?: string; -} - -export default function StatsPanel({ stats, isVisible, className }: StatsPanelProps) { - const [withdrawalRate, setWithdrawalRate] = useState(0.03); // 3% default - - const calculateWithdrawal = (rate: number) => { - if (!stats.currentValue) return 0; - return stats.currentValue * rate; - }; - - const formatCurrency = (amount: number) => { - return new Intl.NumberFormat('de-DE', { - style: 'currency', - currency: 'EUR', - minimumFractionDigits: 2, - }).format(amount); - }; - - const formatPercentage = (percentage: number) => { - return `${percentage >= 0 ? '+' : ''}${percentage.toFixed(2)}%`; - }; - - return ( -
-
-
- - {/* Portfolio Overview */} -
-

- Portfolio -

- -
-
- Shares: - {stats.totalShares.toFixed(6)} -
- -
- Invested: - {formatCurrency(stats.totalInvestment)} -
- -
- Avg Cost: - {formatCurrency(stats.averageCostPerShare)} -
-
-
- - {/* Current Value */} - {stats.currentPrice && ( -
-

- Current Value -

- -
-
- Price: - {formatCurrency(stats.currentPrice)} -
- -
- Value: - {formatCurrency(stats.currentValue || 0)} -
- - {stats.profitLoss !== undefined && ( -
- P&L: - = 0 ? "text-green-400" : "text-red-500" - )}> - {formatCurrency(stats.profitLoss)} - -
- )} - - {stats.profitLossPercentage !== undefined && ( -
- Return: - = 0 ? "text-green-400" : "text-red-500" - )}> - {formatPercentage(stats.profitLossPercentage)} - -
- )} -
-
- )} - - {/* Withdrawal Estimates */} - {stats.currentValue && ( -
-

- Annual Withdrawal -

- -
-
- 3%: - {formatCurrency(calculateWithdrawal(0.03))} -
- -
- 4%: - {formatCurrency(calculateWithdrawal(0.04))} -
- -
- Custom: -
- setWithdrawalRate(Number(e.target.value) / 100)} - className="w-12 bg-transparent text-red-400 text-right text-xs border-b border-red-500/30 focus:border-red-400 outline-none" - min="0" - max="10" - step="0.1" - /> - % -
-
- -
- - {formatCurrency(calculateWithdrawal(withdrawalRate))} -
-
-
- )} - - {/* Monthly Breakdown */} - {stats.currentValue && ( -
-

- Monthly Income -

- -
-
- 3%: - {formatCurrency(calculateWithdrawal(0.03) / 12)} -
- -
- 4%: - {formatCurrency(calculateWithdrawal(0.04) / 12)} -
- -
- Custom: - {formatCurrency(calculateWithdrawal(withdrawalRate) / 12)} -
-
-
- )} -
-
-
- ); -} \ No newline at end of file From 2ea5107106da2c1c864a459a70b8bcab883ec53a Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sun, 13 Jul 2025 00:03:34 +0200 Subject: [PATCH 10/15] Fix the big number --- .../js/components/Display/LedDisplay.tsx | 90 +++++-------------- resources/js/pages/dashboard.tsx | 22 ++--- 2 files changed, 35 insertions(+), 77 deletions(-) diff --git a/resources/js/components/Display/LedDisplay.tsx b/resources/js/components/Display/LedDisplay.tsx index 822242f..77d24a8 100644 --- a/resources/js/components/Display/LedDisplay.tsx +++ b/resources/js/components/Display/LedDisplay.tsx @@ -8,97 +8,53 @@ interface LedDisplayProps { onClick?: () => void; } -export default function LedDisplay({ - value, - className, - animate = true, +export default function LedDisplay({ + value, + className, onClick }: LedDisplayProps) { const [displayValue, setDisplayValue] = useState(0); // Animate number changes useEffect(() => { - if (!animate) { - setDisplayValue(value); - return; - } + setDisplayValue(value); + return; + }, [value]); - const duration = 1000; // 1 second animation - const steps = 60; // 60fps - const stepValue = (value - displayValue) / steps; - - if (Math.abs(stepValue) < 0.01) { - setDisplayValue(value); - return; - } - - const timer = setInterval(() => { - setDisplayValue(prev => { - const next = prev + stepValue; - if (Math.abs(next - value) < Math.abs(stepValue)) { - clearInterval(timer); - return value; - } - return next; - }); - }, duration / steps); - - return () => clearInterval(timer); - }, [value, displayValue, animate]); - - // Format number appropriately for shares + // Format number with zero-padding for consistent width const formatValue = (value: number) => { - // If it's a whole number, show it as integer - if (value % 1 === 0) { - return value.toString(); - } - // Otherwise show up to 6 decimal places, removing trailing zeros - return value.toFixed(6).replace(/\.?0+$/, ''); + // Always pad to 5 digits for consistent display width + const integerPart = Math.floor(value); + return integerPart.toString().padStart(5, '0'); }; - + const formattedValue = formatValue(displayValue); return ( -
-
- {/* Background glow effect */} -
- {formattedValue} -
- - {/* Main LED text */} +
+ "filter brightness-110", + "leading-none", + "transition-all duration-300" + )} + style={{ letterSpacing: '0.15em' }}> {formattedValue}
- - {/* Subtle scan line effect */} -
-
-
-
- - {/* Label */} -
- total shares
); -} \ No newline at end of file +} diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index c9c3c1d..6a295a2 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -128,16 +128,18 @@ export default function Dashboard() { {/* Stacked Layout */} -
-
- {/* Box 1: LED Number Display */} - +
+
+ {/* Box 1: LED Number Display - Fixed position from top */} +
+ +
{/* Box 2: Progress Bar (toggleable) */} -
+
{/* Box 3: Stats Box (toggleable) */} -
+
setActiveForm('purchase')} @@ -154,7 +156,7 @@ export default function Dashboard() {
{/* Box 4: Forms (only when active form is set) */} -
+
setActiveForm(null)} From 6e08bd80edd404900930a6596879a9517b256657 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sun, 13 Jul 2025 00:18:45 +0200 Subject: [PATCH 11/15] Fix milestone issues --- .../Milestones/MilestoneController.php | 32 ++++------ app/Models/Milestone.php | 21 +++++++ ...5_07_12_221324_create_milestones_table.php | 29 +++++++++ .../js/components/Display/ProgressBar.tsx | 31 ---------- resources/js/components/Display/StatsBox.tsx | 42 +++++++++++++ .../Transactions/AddPurchaseForm.tsx | 1 - resources/js/pages/dashboard.tsx | 36 +++++++++-- tests/Feature/MilestoneTest.php | 59 +++++++++++++++++++ 8 files changed, 193 insertions(+), 58 deletions(-) create mode 100644 app/Models/Milestone.php create mode 100644 database/migrations/2025_07_12_221324_create_milestones_table.php create mode 100644 tests/Feature/MilestoneTest.php diff --git a/app/Http/Controllers/Milestones/MilestoneController.php b/app/Http/Controllers/Milestones/MilestoneController.php index aee0ef7..871b9a5 100644 --- a/app/Http/Controllers/Milestones/MilestoneController.php +++ b/app/Http/Controllers/Milestones/MilestoneController.php @@ -3,40 +3,32 @@ namespace App\Http\Controllers\Milestones; use App\Http\Controllers\Controller; +use App\Models\Milestone; use Illuminate\Http\Request; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\JsonResponse; class MilestoneController extends Controller { - /** - * Store a new milestone. - */ - public function store(Request $request): JsonResponse + public function store(Request $request): RedirectResponse { $request->validate([ 'target' => 'required|integer|min:1', 'description' => 'required|string|max:255', ]); - // For now, just return success without persisting to database - // This allows the frontend form to work properly - return response()->json([ - 'message' => 'Milestone created successfully', - 'milestone' => [ - 'target' => $request->target, - 'description' => $request->description, - 'created_at' => now(), - ] - ], 201); + Milestone::create([ + 'target' => $request->target, + 'description' => $request->description, + ]); + + return back()->with('success', 'Milestone created successfully'); } - /** - * Get all milestones. - */ public function index(): JsonResponse { - // For now, return empty array - // Later this could fetch from database - return response()->json([]); + $milestones = Milestone::orderBy('target')->get(); + + return response()->json($milestones); } } \ No newline at end of file diff --git a/app/Models/Milestone.php b/app/Models/Milestone.php new file mode 100644 index 0000000..98efa38 --- /dev/null +++ b/app/Models/Milestone.php @@ -0,0 +1,21 @@ + 'integer', + ]; +} diff --git a/database/migrations/2025_07_12_221324_create_milestones_table.php b/database/migrations/2025_07_12_221324_create_milestones_table.php new file mode 100644 index 0000000..da034cc --- /dev/null +++ b/database/migrations/2025_07_12_221324_create_milestones_table.php @@ -0,0 +1,29 @@ +id(); + $table->integer('target'); + $table->string('description'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('milestones'); + } +}; diff --git a/resources/js/components/Display/ProgressBar.tsx b/resources/js/components/Display/ProgressBar.tsx index a777d3a..97e0b4b 100644 --- a/resources/js/components/Display/ProgressBar.tsx +++ b/resources/js/components/Display/ProgressBar.tsx @@ -1,45 +1,14 @@ import { cn } from '@/lib/utils'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; -import { useState } from 'react'; interface ProgressBarProps { - value: number; className?: string; onClick?: () => void; } export default function ProgressBar({ - value, className, onClick }: ProgressBarProps) { - const [currentMilestoneIndex, setCurrentMilestoneIndex] = useState(0); - - // Milestone definitions - const milestones = [ - { target: 1500, label: '1.5K', color: 'bg-blue-500' }, - { target: 3000, label: '3K', color: 'bg-green-500' }, - { target: 4500, label: '4.5K', color: 'bg-yellow-500' }, - { target: 6000, label: '6K', color: 'bg-red-500' }, - ]; - - const currentMilestone = milestones[currentMilestoneIndex]; - const progress = Math.min((value / currentMilestone.target) * 100, 100); - const isCompleted = value >= currentMilestone.target; - - // Milestone navigation - const nextMilestone = () => { - setCurrentMilestoneIndex((prev) => - prev < milestones.length - 1 ? prev + 1 : 0 - ); - }; - - const prevMilestone = () => { - setCurrentMilestoneIndex((prev) => - prev > 0 ? prev - 1 : milestones.length - 1 - ); - }; - return (
void; onAddMilestone?: () => void; @@ -18,6 +25,7 @@ interface StatsBoxProps { export default function StatsBox({ stats, + milestones = [], className, onAddPurchase, onAddMilestone @@ -123,6 +131,40 @@ export default function StatsBox({
)} + {/* Milestones */} + {milestones.length > 0 && ( +
+
Milestones
+
+ {milestones.map((milestone, index) => { + const isReached = stats.totalShares >= milestone.target; + return ( +
+
+
+ + {milestone.target.toLocaleString()} + +
+
+ {milestone.description} +
+
+ ); + })} +
+
+ )} + {/* Action Buttons */}
diff --git a/resources/js/components/Transactions/AddPurchaseForm.tsx b/resources/js/components/Transactions/AddPurchaseForm.tsx index edc42ba..68732b2 100644 --- a/resources/js/components/Transactions/AddPurchaseForm.tsx +++ b/resources/js/components/Transactions/AddPurchaseForm.tsx @@ -1,5 +1,4 @@ import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import InputError from '@/components/InputError'; diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index 6a295a2..b2b0f80 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -15,6 +15,12 @@ interface CurrentPrice { current_price: number | null; } +interface Milestone { + target: number; + description: string; + created_at: string; +} + export default function Dashboard() { const [purchaseData, setPurchaseData] = useState({ total_shares: 0, @@ -26,18 +32,20 @@ export default function Dashboard() { current_price: null, }); + const [milestones, setMilestones] = useState([]); const [showProgressBar, setShowProgressBar] = useState(false); const [showStatsBox, setShowStatsBox] = useState(false); const [activeForm, setActiveForm] = useState<'purchase' | 'milestone' | null>(null); const [loading, setLoading] = useState(true); - // Fetch purchase summary and current price + // Fetch purchase summary, current price, and milestones useEffect(() => { const fetchData = async () => { try { - const [purchaseResponse, priceResponse] = await Promise.all([ + const [purchaseResponse, priceResponse, milestonesResponse] = await Promise.all([ fetch('/purchases/summary'), fetch('/pricing/current'), + fetch('/milestones'), ]); if (purchaseResponse.ok) { @@ -49,6 +57,11 @@ export default function Dashboard() { const price = await priceResponse.json(); setPriceData(price); } + + if (milestonesResponse.ok) { + const milestonesData = await milestonesResponse.json(); + setMilestones(milestonesData); + } } catch (error) { console.error('Failed to fetch data:', error); } finally { @@ -72,6 +85,19 @@ export default function Dashboard() { } }; + // Refresh milestones after successful creation + const handleMilestoneSuccess = async () => { + try { + const milestonesResponse = await fetch('/milestones'); + if (milestonesResponse.ok) { + const milestonesData = await milestonesResponse.json(); + setMilestones(milestonesData); + } + } catch (error) { + console.error('Failed to refresh milestone data:', error); + } + }; + // Calculate portfolio stats const currentValue = priceData.current_price @@ -141,7 +167,6 @@ export default function Dashboard() { {/* Box 2: Progress Bar (toggleable) */}
@@ -150,6 +175,7 @@ export default function Dashboard() {
setActiveForm('purchase')} onAddMilestone={() => setActiveForm('milestone')} /> @@ -161,9 +187,7 @@ export default function Dashboard() { type={activeForm} onClose={() => setActiveForm(null)} onPurchaseSuccess={handlePurchaseSuccess} - onMilestoneSuccess={() => { - console.log('Milestone added successfully'); - }} + onMilestoneSuccess={handleMilestoneSuccess} />
diff --git a/tests/Feature/MilestoneTest.php b/tests/Feature/MilestoneTest.php new file mode 100644 index 0000000..d86bb84 --- /dev/null +++ b/tests/Feature/MilestoneTest.php @@ -0,0 +1,59 @@ + 1500, + 'description' => 'First milestone' + ]); + + $this->assertDatabaseHas('milestones', [ + 'target' => 1500, + 'description' => 'First milestone' + ]); + + $this->assertEquals(1500, $milestone->target); + $this->assertEquals('First milestone', $milestone->description); + } + + public function test_can_fetch_milestones_via_api(): void + { + // Create test milestones + Milestone::create(['target' => 1500, 'description' => 'First milestone']); + Milestone::create(['target' => 3000, 'description' => 'Second milestone']); + + $response = $this->get('/milestones'); + + $response->assertStatus(200); + $response->assertJsonCount(2); + $response->assertJson([ + ['target' => 1500, 'description' => 'First milestone'], + ['target' => 3000, 'description' => 'Second milestone'] + ]); + } + + public function test_milestones_ordered_by_target(): void + { + // Create milestones in reverse order + Milestone::create(['target' => 3000, 'description' => 'Third']); + Milestone::create(['target' => 1000, 'description' => 'First']); + Milestone::create(['target' => 2000, 'description' => 'Second']); + + $response = $this->get('/milestones'); + + $milestones = $response->json(); + $this->assertEquals(1000, $milestones[0]['target']); + $this->assertEquals(2000, $milestones[1]['target']); + $this->assertEquals(3000, $milestones[2]['target']); + } +} From 31c62b23e17d79594f940e3be48e38bbfdc2db76 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sun, 13 Jul 2025 00:30:19 +0200 Subject: [PATCH 12/15] Fix progress bar styling --- .../js/components/Display/ProgressBar.tsx | 48 ++++++++++++++++--- resources/js/pages/dashboard.tsx | 2 + 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/resources/js/components/Display/ProgressBar.tsx b/resources/js/components/Display/ProgressBar.tsx index 97e0b4b..45c8fcf 100644 --- a/resources/js/components/Display/ProgressBar.tsx +++ b/resources/js/components/Display/ProgressBar.tsx @@ -1,28 +1,64 @@ import { cn } from '@/lib/utils'; +interface Milestone { + target: number; + description: string; + created_at: string; +} + interface ProgressBarProps { + currentShares: number; + milestones: Milestone[]; className?: string; onClick?: () => void; } export default function ProgressBar({ + currentShares, + milestones, className, onClick }: ProgressBarProps) { + // Get the first milestone (lowest target) for progress calculation + const firstMilestone = milestones.length > 0 ? milestones[0] : null; + + // Calculate progress percentage + const progressPercentage = firstMilestone + ? Math.min((currentShares / firstMilestone.target) * 100, 100) + : 0; return (
-
- PROGRESS BAR + {/* Progress Bar Container */} +
+ {/* Old-school progress bar with overlaid text */} +
+ {/* Inner container */} +
+ {/* Progress fill */} +
+ + {/* Text overlay */} + {firstMilestone && ( +
+ {/* Base text (red on black background) */} +
+ {progressPercentage.toFixed(1)}% +
+
+ )} +
+
); diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index b2b0f80..4275e3b 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -167,6 +167,8 @@ export default function Dashboard() { {/* Box 2: Progress Bar (toggleable) */}
From 6c1f4a14c35ad0c5629db4b3cca83815d7e6de59 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sun, 13 Jul 2025 01:07:16 +0200 Subject: [PATCH 13/15] Move buttons into dropdown --- .../Controllers/Pricing/PricingController.php | 8 +- .../js/components/Display/InlineForm.tsx | 16 +- resources/js/components/Display/StatsBox.tsx | 262 +++++++++--------- .../js/components/Pricing/UpdatePriceForm.tsx | 4 +- resources/js/pages/dashboard.tsx | 17 +- 5 files changed, 163 insertions(+), 144 deletions(-) diff --git a/app/Http/Controllers/Pricing/PricingController.php b/app/Http/Controllers/Pricing/PricingController.php index cdd34a3..bb07156 100644 --- a/app/Http/Controllers/Pricing/PricingController.php +++ b/app/Http/Controllers/Pricing/PricingController.php @@ -18,7 +18,7 @@ public function current(): JsonResponse ]); } - public function update(Request $request): JsonResponse + public function update(Request $request) { $validated = $request->validate([ 'date' => 'required|date|before_or_equal:today', @@ -27,11 +27,7 @@ public function update(Request $request): JsonResponse $assetPrice = AssetPrice::updatePrice($validated['date'], $validated['price']); - return response()->json([ - 'success' => true, - 'message' => 'Asset price updated successfully!', - 'data' => $assetPrice, - ]); + return back()->with('success', 'Asset price updated successfully!'); } public function history(Request $request): JsonResponse diff --git a/resources/js/components/Display/InlineForm.tsx b/resources/js/components/Display/InlineForm.tsx index 949de4d..9d6b693 100644 --- a/resources/js/components/Display/InlineForm.tsx +++ b/resources/js/components/Display/InlineForm.tsx @@ -1,13 +1,15 @@ import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm'; import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm'; +import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm'; import { cn } from '@/lib/utils'; import { X } from 'lucide-react'; interface InlineFormProps { - type: 'purchase' | 'milestone' | null; + type: 'purchase' | 'milestone' | 'price' | null; onClose: () => void; onPurchaseSuccess?: () => void; onMilestoneSuccess?: () => void; + onPriceSuccess?: () => void; className?: string; } @@ -16,11 +18,12 @@ export default function InlineForm({ onClose, onPurchaseSuccess, onMilestoneSuccess, + onPriceSuccess, className }: InlineFormProps) { if (!type) return null; - const title = type === 'purchase' ? 'ADD PURCHASE' : 'ADD MILESTONE'; + const title = type === 'purchase' ? 'ADD PURCHASE' : type === 'milestone' ? 'ADD MILESTONE' : 'UPDATE PRICE'; return (
- ) : ( + ) : type === 'milestone' ? ( { if (onMilestoneSuccess) onMilestoneSuccess(); onClose(); }} /> + ) : ( + { + if (onPriceSuccess) onPriceSuccess(); + onClose(); + }} + /> )}
diff --git a/resources/js/components/Display/StatsBox.tsx b/resources/js/components/Display/StatsBox.tsx index b88d0bf..4d11085 100644 --- a/resources/js/components/Display/StatsBox.tsx +++ b/resources/js/components/Display/StatsBox.tsx @@ -1,5 +1,6 @@ import { cn } from '@/lib/utils'; import { Plus } from 'lucide-react'; +import { useState } from 'react'; interface Milestone { target: number; @@ -21,6 +22,7 @@ interface StatsBoxProps { className?: string; onAddPurchase?: () => void; onAddMilestone?: () => void; + onUpdatePrice?: () => void; } export default function StatsBox({ @@ -28,8 +30,10 @@ export default function StatsBox({ milestones = [], className, onAddPurchase, - onAddMilestone + onAddMilestone, + onUpdatePrice }: StatsBoxProps) { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); const formatCurrency = (amount: number) => { return new Intl.NumberFormat('de-DE', { style: 'currency', @@ -50,149 +54,141 @@ export default function StatsBox({ return (
- {/* Current Price */} - {stats.currentPrice && ( -
-
- current price -
-
- {formatCurrencyDetailed(stats.currentPrice)} -
-
- )} - - {/* Portfolio Stats Grid */} -
- {/* Total Investment */} -
-
Total Investment
-
- {formatCurrency(stats.totalInvestment)} -
-
- - {/* Current Value */} - {stats.currentValue && ( -
-
Current Value
-
- {formatCurrency(stats.currentValue)} -
-
- )} - - {/* Average Cost */} -
-
Avg Cost/Share
-
- {formatCurrencyDetailed(stats.averageCostPerShare)} -
-
- - {/* Profit/Loss */} - {stats.profitLoss !== undefined && ( -
-
P&L
-
= 0 ? "text-green-400" : "text-red-400" - )}> - {stats.profitLoss >= 0 ? '+' : ''}{formatCurrency(stats.profitLoss)} -
-
- )} -
- - {/* Withdrawal Estimates */} - {stats.currentValue && ( -
-
Annual Withdrawal (Safe)
-
-
-
3% Rule
-
- {formatCurrency(stats.currentValue * 0.03)} +
+ {/* STATS Title and Current Price */} +
+

+ STATS +

+
+ {stats.currentPrice && ( +
+ VWCE: {formatCurrencyDetailed(stats.currentPrice)}
-
-
-
4% Rule
-
- {formatCurrency(stats.currentValue * 0.04)} -
-
-
-
- )} - - {/* Milestones */} - {milestones.length > 0 && ( -
-
Milestones
-
- {milestones.map((milestone, index) => { - const isReached = stats.totalShares >= milestone.target; - return ( -
-
-
- - {milestone.target.toLocaleString()} - -
-
- {milestone.description} -
+ )} + + {/* Action Dropdown */} +
+ + + {/* Dropdown Menu */} + {isDropdownOpen && ( +
+ {onAddPurchase && ( + + )} + {onAddMilestone && ( + + )} + {onUpdatePrice && ( + + )}
- ); - })} + )} +
- )} - {/* Action Buttons */} + {/* Milestone Table */}
-
- {/* Add Purchase Button */} - {onAddPurchase && ( - - )} - - {/* Add Milestone Button */} - {onAddMilestone && ( - - )} +
MILESTONES
+
+ + + + + + + + + + + {/* Create combined array with current position and milestones, sorted by target */} + {[ + ...milestones.map(m => ({ ...m, isCurrent: false })), + { + target: stats.totalShares, + description: 'CURRENT', + created_at: '', + isCurrent: true + } + ] + .sort((a, b) => a.target - b.target) + .map((item, index) => { + const swr3 = stats.currentPrice ? item.target * stats.currentPrice * 0.03 : 0; + const swr4 = stats.currentPrice ? item.target * stats.currentPrice * 0.04 : 0; + + return ( + = item.target + ? "text-green-400/80" + : "text-red-400/70" + )} + > + + + + + + ); + })} + +
DESCRIPTIONSHARESSWR 3%SWR 4%
+ {item.isCurrent ? ( + {item.description} + ) : ( + item.description + )} + + {Math.floor(item.target).toLocaleString()} + + {stats.currentPrice ? formatCurrency(swr3) : 'N/A'} + + {stats.currentPrice ? formatCurrency(swr4) : 'N/A'} +
+
); } \ No newline at end of file diff --git a/resources/js/components/Pricing/UpdatePriceForm.tsx b/resources/js/components/Pricing/UpdatePriceForm.tsx index b4944e5..bd18cb9 100644 --- a/resources/js/components/Pricing/UpdatePriceForm.tsx +++ b/resources/js/components/Pricing/UpdatePriceForm.tsx @@ -16,9 +16,10 @@ interface PriceUpdateFormData { interface UpdatePriceFormProps { currentPrice?: number; className?: string; + onSuccess?: () => void; } -export default function UpdatePriceForm({ currentPrice, className }: UpdatePriceFormProps) { +export default function UpdatePriceForm({ currentPrice, className, onSuccess }: UpdatePriceFormProps) { const { data, setData, post, processing, errors } = useForm({ date: new Date().toISOString().split('T')[0], // Today's date in YYYY-MM-DD format price: currentPrice?.toString() || '', @@ -31,6 +32,7 @@ export default function UpdatePriceForm({ currentPrice, className }: UpdatePrice onSuccess: () => { // Keep the date, reset only price if needed // User might want to update same day multiple times + if (onSuccess) onSuccess(); }, }); }; diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index 4275e3b..1727698 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -35,7 +35,7 @@ export default function Dashboard() { const [milestones, setMilestones] = useState([]); const [showProgressBar, setShowProgressBar] = useState(false); const [showStatsBox, setShowStatsBox] = useState(false); - const [activeForm, setActiveForm] = useState<'purchase' | 'milestone' | null>(null); + const [activeForm, setActiveForm] = useState<'purchase' | 'milestone' | 'price' | null>(null); const [loading, setLoading] = useState(true); // Fetch purchase summary, current price, and milestones @@ -98,6 +98,19 @@ export default function Dashboard() { } }; + // Refresh price data after successful update + const handlePriceSuccess = async () => { + try { + const priceResponse = await fetch('/pricing/current'); + if (priceResponse.ok) { + const price = await priceResponse.json(); + setPriceData(price); + } + } catch (error) { + console.error('Failed to refresh price data:', error); + } + }; + // Calculate portfolio stats const currentValue = priceData.current_price @@ -180,6 +193,7 @@ export default function Dashboard() { milestones={milestones} onAddPurchase={() => setActiveForm('purchase')} onAddMilestone={() => setActiveForm('milestone')} + onUpdatePrice={() => setActiveForm('price')} />
@@ -190,6 +204,7 @@ export default function Dashboard() { onClose={() => setActiveForm(null)} onPurchaseSuccess={handlePurchaseSuccess} onMilestoneSuccess={handleMilestoneSuccess} + onPriceSuccess={handlePriceSuccess} />
From 0f720d7c91bec58e4f6c08281b4a9f372e090018 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sun, 13 Jul 2025 01:13:36 +0200 Subject: [PATCH 14/15] Cycle milestones --- .../js/components/Display/ProgressBar.tsx | 14 ++-- resources/js/components/Display/StatsBox.tsx | 70 +++++++++++++------ resources/js/pages/dashboard.tsx | 11 +++ 3 files changed, 69 insertions(+), 26 deletions(-) diff --git a/resources/js/components/Display/ProgressBar.tsx b/resources/js/components/Display/ProgressBar.tsx index 45c8fcf..05fdf7e 100644 --- a/resources/js/components/Display/ProgressBar.tsx +++ b/resources/js/components/Display/ProgressBar.tsx @@ -9,6 +9,7 @@ interface Milestone { interface ProgressBarProps { currentShares: number; milestones: Milestone[]; + selectedMilestoneIndex?: number; className?: string; onClick?: () => void; } @@ -16,15 +17,18 @@ interface ProgressBarProps { export default function ProgressBar({ currentShares, milestones, + selectedMilestoneIndex = 0, className, onClick }: ProgressBarProps) { - // Get the first milestone (lowest target) for progress calculation - const firstMilestone = milestones.length > 0 ? milestones[0] : null; + // Get the selected milestone for progress calculation + const selectedMilestone = milestones.length > 0 && selectedMilestoneIndex < milestones.length + ? milestones[selectedMilestoneIndex] + : null; // Calculate progress percentage - const progressPercentage = firstMilestone - ? Math.min((currentShares / firstMilestone.target) * 100, 100) + const progressPercentage = selectedMilestone + ? Math.min((currentShares / selectedMilestone.target) * 100, 100) : 0; return (
{/* Text overlay */} - {firstMilestone && ( + {selectedMilestone && (
{/* Base text (red on black background) */}
diff --git a/resources/js/components/Display/StatsBox.tsx b/resources/js/components/Display/StatsBox.tsx index 4d11085..ef690a5 100644 --- a/resources/js/components/Display/StatsBox.tsx +++ b/resources/js/components/Display/StatsBox.tsx @@ -1,5 +1,5 @@ import { cn } from '@/lib/utils'; -import { Plus } from 'lucide-react'; +import { Plus, ChevronRight } from 'lucide-react'; import { useState } from 'react'; interface Milestone { @@ -19,21 +19,31 @@ interface StatsBoxProps { profitLossPercentage?: number; }; milestones?: Milestone[]; + selectedMilestoneIndex?: number; + onMilestoneSelect?: (index: number) => void; className?: string; onAddPurchase?: () => void; onAddMilestone?: () => void; onUpdatePrice?: () => void; } -export default function StatsBox({ - stats, +export default function StatsBox({ + stats, milestones = [], + selectedMilestoneIndex = 0, + onMilestoneSelect, className, onAddPurchase, onAddMilestone, onUpdatePrice }: StatsBoxProps) { const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + const handleCycleMilestone = () => { + if (milestones.length === 0 || !onMilestoneSelect) return; + const nextIndex = (selectedMilestoneIndex + 1) % milestones.length; + onMilestoneSelect(nextIndex); + }; const formatCurrency = (amount: number) => { return new Intl.NumberFormat('de-DE', { style: 'currency', @@ -52,7 +62,7 @@ export default function StatsBox({ }; return ( -
STATS -
+
{stats.currentPrice && (
VWCE: {formatCurrencyDetailed(stats.currentPrice)}
)} - + {/* Action Dropdown */}
- + {/* Dropdown Menu */} {isDropdownOpen && (
@@ -121,6 +131,17 @@ export default function StatsBox({
)}
+ + {/* Milestone Cycle Button */} + {milestones.length > 1 && ( + + )}
@@ -141,28 +162,35 @@ export default function StatsBox({ {/* Create combined array with current position and milestones, sorted by target */} {[ ...milestones.map(m => ({ ...m, isCurrent: false })), - { - target: stats.totalShares, - description: 'CURRENT', - created_at: '', - isCurrent: true + { + target: stats.totalShares, + description: 'CURRENT', + created_at: '', + isCurrent: true } ] .sort((a, b) => a.target - b.target) .map((item, index) => { const swr3 = stats.currentPrice ? item.target * stats.currentPrice * 0.03 : 0; const swr4 = stats.currentPrice ? item.target * stats.currentPrice * 0.04 : 0; - + + // Check if this milestone is the selected one for progress bar + const isSelectedMilestone = !item.isCurrent && milestones.findIndex(m => + m.target === item.target && m.description === item.description + ) === selectedMilestoneIndex; + return ( - = item.target - ? "text-green-400/80" - : "text-red-400/70" + item.isCurrent + ? "bg-red-500/10 text-red-300" + : isSelectedMilestone + ? "bg-blue-500/10 text-blue-300 border-blue-500/30" + : stats.totalShares >= item.target + ? "text-green-400/80" + : "text-red-400/70" )} > @@ -191,4 +219,4 @@ export default function StatsBox({
); -} \ No newline at end of file +} diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index 1727698..6753014 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -33,6 +33,7 @@ export default function Dashboard() { }); const [milestones, setMilestones] = useState([]); + const [selectedMilestoneIndex, setSelectedMilestoneIndex] = useState(0); const [showProgressBar, setShowProgressBar] = useState(false); const [showStatsBox, setShowStatsBox] = useState(false); const [activeForm, setActiveForm] = useState<'purchase' | 'milestone' | 'price' | null>(null); @@ -92,12 +93,19 @@ export default function Dashboard() { if (milestonesResponse.ok) { const milestonesData = await milestonesResponse.json(); setMilestones(milestonesData); + // Reset to first milestone when milestones change + setSelectedMilestoneIndex(0); } } catch (error) { console.error('Failed to refresh milestone data:', error); } }; + // Handle milestone selection + const handleMilestoneSelect = (index: number) => { + setSelectedMilestoneIndex(index); + }; + // Refresh price data after successful update const handlePriceSuccess = async () => { try { @@ -182,6 +190,7 @@ export default function Dashboard() {
@@ -191,6 +200,8 @@ export default function Dashboard() { setActiveForm('purchase')} onAddMilestone={() => setActiveForm('milestone')} onUpdatePrice={() => setActiveForm('price')} From 00da6cab8095809247b4f964c394322874995011 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sun, 13 Jul 2025 01:27:02 +0200 Subject: [PATCH 15/15] Fix stats layout --- resources/js/components/Display/StatsBox.tsx | 86 +++++++++----------- 1 file changed, 39 insertions(+), 47 deletions(-) diff --git a/resources/js/components/Display/StatsBox.tsx b/resources/js/components/Display/StatsBox.tsx index ef690a5..eafa4aa 100644 --- a/resources/js/components/Display/StatsBox.tsx +++ b/resources/js/components/Display/StatsBox.tsx @@ -86,7 +86,7 @@ export default function StatsBox({
@@ -123,7 +123,7 @@ export default function StatsBox({ onUpdatePrice(); setIsDropdownOpen(false); }} - className="w-full text-left px-4 py-2 text-green-400 hover:bg-green-600/20 hover:text-green-300 transition-colors text-sm font-mono border-b border-red-500/20 last:border-b-0" + className="w-full text-left px-4 py-2 text-red-400 hover:bg-red-600/20 hover:text-red-300 transition-colors transition-colors text-sm font-mono border-b border-red-500/20 last:border-b-0" > UPDATE PRICE @@ -136,7 +136,7 @@ export default function StatsBox({ {milestones.length > 1 && (
{/* Milestone Table */} -
-
MILESTONES
+
+
MILESTONES
- - - - - + + + + + - {/* Create combined array with current position and milestones, sorted by target */} - {[ - ...milestones.map(m => ({ ...m, isCurrent: false })), - { - target: stats.totalShares, - description: 'CURRENT', - created_at: '', - isCurrent: true - } - ] - .sort((a, b) => a.target - b.target) - .map((item, index) => { - const swr3 = stats.currentPrice ? item.target * stats.currentPrice * 0.03 : 0; - const swr4 = stats.currentPrice ? item.target * stats.currentPrice * 0.04 : 0; + {/* Current position row */} + + + + + + - // Check if this milestone is the selected one for progress bar - const isSelectedMilestone = !item.isCurrent && milestones.findIndex(m => - m.target === item.target && m.description === item.description - ) === selectedMilestoneIndex; + {/* Render milestones after current */} + {milestones.map((milestone, index) => { + const swr3 = stats.currentPrice ? milestone.target * stats.currentPrice * 0.03 : 0; + const swr4 = stats.currentPrice ? milestone.target * stats.currentPrice * 0.04 : 0; + + const isSelectedMilestone = index === selectedMilestoneIndex; return ( = item.target - ? "text-green-400/80" - : "text-red-400/70" + isSelectedMilestone + ? "text-red-500 font-bold" + : "bg-red-500 text-black" )} > - - - -
DESCRIPTIONSHARESSWR 3%SWR 4%
DESCRIPTIONSHARESSWR 3%SWR 4%
CURRENT + {Math.floor(stats.totalShares).toLocaleString()} + + {stats.currentPrice ? formatCurrency(stats.totalShares * stats.currentPrice * 0.03) : 'N/A'} + + {stats.currentPrice ? formatCurrency(stats.totalShares * stats.currentPrice * 0.04) : 'N/A'} +
- {item.isCurrent ? ( - {item.description} - ) : ( - item.description - )} + + {milestone.description} - {Math.floor(item.target).toLocaleString()} + + {Math.floor(milestone.target).toLocaleString()} + {stats.currentPrice ? formatCurrency(swr3) : 'N/A'} + {stats.currentPrice ? formatCurrency(swr4) : 'N/A'}