From b673d54ededc76a9770e9334c36ce0ffba66394b Mon Sep 17 00:00:00 2001 From: Doh Date: Mon, 26 May 2025 08:42:00 -0400 Subject: [PATCH] Added improved loading screen (#148) --- CHANGELOG.md | 7 ++++ app/layout.tsx | 5 +-- components/ClientWrapper.tsx | 12 ++++++- components/DesktopNavDisplay.tsx | 42 ++++++++++++++++++++++ components/Header.tsx | 43 ++-------------------- components/HeaderActions.tsx | 38 ++++++++++++++++++++ components/LoadingSpinner.tsx | 61 +++++++++++++++++++++++++++++++ components/MobileNavDisplay.tsx | 45 +++++++++++++++++++++++ components/Navigation.tsx | 62 +++++++++----------------------- package.json | 2 +- 10 files changed, 226 insertions(+), 91 deletions(-) create mode 100644 components/DesktopNavDisplay.tsx create mode 100644 components/HeaderActions.tsx create mode 100644 components/LoadingSpinner.tsx create mode 100644 components/MobileNavDisplay.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index d2f35bd..268ec6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Version 0.2.18 + +### Improved + +* nicer loading UI (#147) +* header and navigation code refactor + ## Version 0.2.17 ### Fixed diff --git a/app/layout.tsx b/app/layout.tsx index d7bdd52..572ad55 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,7 +2,6 @@ import './globals.css' import { Inter } from 'next/font/google' import { DM_Sans } from 'next/font/google' import { JotaiProvider } from '@/components/jotai-providers' -import { Suspense } from 'react' import { JotaiHydrate } from '@/components/jotai-hydrate' import { loadSettings, loadHabitsData, loadCoinsData, loadWishlistData, loadUsersData, loadServerSettings } from './actions/data' import Layout from '@/components/Layout' @@ -11,6 +10,8 @@ import { ThemeProvider } from "@/components/theme-provider" import { SessionProvider } from 'next-auth/react' import { NextIntlClientProvider } from 'next-intl'; import { getLocale, getMessages } from 'next-intl/server'; +import { Suspense } from 'react' +import LoadingSpinner from '@/components/LoadingSpinner' // Inter (clean, modern, excellent readability) @@ -75,7 +76,7 @@ export default async function RootLayout({ }} /> - + }> { + setIsMounted(true); + }, []); useEffect(() => { if (status === 'loading') return @@ -22,6 +29,9 @@ export default function ClientWrapper({ children }: { children: ReactNode }) { } }, [currentUserId, status, userSelect]) + if (!isMounted) { + return + } return ( <> {children} diff --git a/components/DesktopNavDisplay.tsx b/components/DesktopNavDisplay.tsx new file mode 100644 index 0000000..a7a7ee8 --- /dev/null +++ b/components/DesktopNavDisplay.tsx @@ -0,0 +1,42 @@ +import Link from 'next/link' +import type { ElementType } from 'react' + +export interface NavItemType { + icon: ElementType; + label: string; + href: string; + position: 'main' | 'bottom'; +} + +interface DesktopNavDisplayProps { + navItems: NavItemType[]; + className?: string; +} + +export default function DesktopNavDisplay({ navItems, className }: DesktopNavDisplayProps) { + // Filter for items relevant to desktop view, typically 'main' position + const desktopNavItems = navItems.filter(item => item.position === 'main'); + + return ( +
+
+
+
+ +
+
+
+
+ ); +} diff --git a/components/Header.tsx b/components/Header.tsx index 6028ab1..5130e89 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,36 +1,13 @@ -'use client' - -import { useEffect, useState } from 'react' -import { useAtom } from 'jotai' -import { coinsAtom, settingsAtom, browserSettingsAtom } from '@/lib/atoms' -import { useCoins } from '@/hooks/useCoins' -import { FormattedNumber } from '@/components/FormattedNumber' -import { Menu, Settings, User, Info, Coins } from 'lucide-react' -import { Button } from '@/components/ui/button' import { Logo } from '@/components/Logo' -import NotificationBell from './NotificationBell' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu' -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import Link from 'next/link' -import dynamic from 'next/dynamic' -import { Profile } from './Profile' -import { useHelpers } from '@/lib/client-helpers' +import HeaderActions from './HeaderActions' interface HeaderProps { className?: string } -const TodayEarnedCoins = dynamic(() => import('./TodayEarnedCoins'), { ssr: false }) export default function Header({ className }: HeaderProps) { - const [settings] = useAtom(settingsAtom) - const [browserSettings] = useAtom(browserSettingsAtom) - const { balance } = useCoins() return ( <>
@@ -39,23 +16,7 @@ export default function Header({ className }: HeaderProps) { -
- - -
- -
- -
-
- - - -
+
diff --git a/components/HeaderActions.tsx b/components/HeaderActions.tsx new file mode 100644 index 0000000..c45890a --- /dev/null +++ b/components/HeaderActions.tsx @@ -0,0 +1,38 @@ +'use client' + +import Link from 'next/link' +import { useAtom } from 'jotai' +import { settingsAtom } from '@/lib/atoms' +import { useCoins } from '@/hooks/useCoins' +import { FormattedNumber } from '@/components/FormattedNumber' +import { Coins } from 'lucide-react' +import NotificationBell from './NotificationBell' +import dynamic from 'next/dynamic' +import { Profile } from './Profile' + +const TodayEarnedCoins = dynamic(() => import('./TodayEarnedCoins'), { ssr: false }) + +export default function HeaderActions() { + const [settings] = useAtom(settingsAtom) + const { balance } = useCoins() + + return ( +
+ + +
+ +
+ +
+
+ + + +
+ ) +} diff --git a/components/LoadingSpinner.tsx b/components/LoadingSpinner.tsx new file mode 100644 index 0000000..0aea6d9 --- /dev/null +++ b/components/LoadingSpinner.tsx @@ -0,0 +1,61 @@ +'use client' + +import React, { useEffect, useState } from 'react'; +import { Coins } from 'lucide-react'; +import { Logo } from '@/components/Logo'; + +const subtexts = [ + "Unearthing your treasures", + "Polishing your gems", + "Mining for good habits", + "Stumbling upon brilliance", + "Discovering your potential", + "Crafting your success story", + "Forging new paths", + "Summoning success", + "Brewing brilliance", + "Charging up your awesome", + "Assembling achievements", + "Leveling up your day", + "Questing for quality", + "Unlocking awesomeness", + "Plotting your progress", +]; + +const LoadingSpinner: React.FC = () => { + const [currentSubtext, setCurrentSubtext] = useState('Loading your data'); + const [animatedDots, setAnimatedDots] = useState(''); + + useEffect(() => { + const randomIndex = Math.floor(Math.random() * subtexts.length); + setCurrentSubtext(subtexts[randomIndex]); + + const dotAnimationInterval = setInterval(() => { + setAnimatedDots(prevDots => { + if (prevDots.length >= 3) { + return ''; + } + return prevDots + '.'; + }); + }, 200); // Adjust timing as needed + + return () => clearInterval(dotAnimationInterval); + }, []); + + + return ( +
+
+ + + {currentSubtext && ( +

+ {currentSubtext}{animatedDots} +

+ )} +
+
+ ); +}; + +export default LoadingSpinner; diff --git a/components/MobileNavDisplay.tsx b/components/MobileNavDisplay.tsx new file mode 100644 index 0000000..5a3acf5 --- /dev/null +++ b/components/MobileNavDisplay.tsx @@ -0,0 +1,45 @@ +import Link from 'next/link' +import type { ElementType } from 'react' + +export interface NavItemType { + icon: ElementType; + label: string; + href: string; + position: 'main' | 'bottom'; +} + +interface MobileNavDisplayProps { + navItems: NavItemType[]; + isIOS: boolean; +} + +export default function MobileNavDisplay({ navItems, isIOS }: MobileNavDisplayProps) { + // Filter for items relevant to mobile view, typically 'main' and 'bottom' positions + const mobileNavItems = navItems.filter(item => item.position === 'main' || item.position === 'bottom'); + // The original code spread main and bottom items separately, effectively concatenating them. + // If specific ordering or duplication was intended, that logic would be here. + // For now, a simple filter and map should suffice if all items are distinct. + // The original code: [...navItems(isTasksView).filter(item => item.position === 'main'), ...navItems(isTasksView).filter(item => item.position === 'bottom')] + // This implies that items could be in 'main' or 'bottom'. The current navItems only have 'main'. + // A simple combined list is fine. + + return ( + <> +
{/* Add padding at the bottom to prevent content from being hidden */} + + + ); +} diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 7c1fa02..769ebf7 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -1,16 +1,24 @@ 'use client' -import Link from 'next/link' -import { Home, Calendar, List, Gift, Coins, Settings, Info, CheckSquare } from 'lucide-react' +import { Home, Calendar, Gift, Coins } from 'lucide-react' import { useAtom } from 'jotai' import { browserSettingsAtom } from '@/lib/atoms' -import { useEffect, useState } from 'react' +import { useEffect, useState, ElementType } from 'react' import { useTranslations } from 'next-intl' import { HabitIcon, TaskIcon } from '@/lib/constants' import { useHelpers } from '@/lib/client-helpers' +import MobileNavDisplay from './MobileNavDisplay' +import DesktopNavDisplay from './DesktopNavDisplay' type ViewPort = 'main' | 'mobile' +export interface NavItemType { + icon: ElementType; + label: string; + href: string; + position: 'main' | 'bottom'; +} + interface NavigationProps { className?: string viewPort: ViewPort @@ -18,13 +26,12 @@ interface NavigationProps { export default function Navigation({ className, viewPort }: NavigationProps) { const t = useTranslations('Navigation') - const [showAbout, setShowAbout] = useState(false) const [isMobileView, setIsMobileView] = useState(false) const [browserSettings] = useAtom(browserSettingsAtom) const isTasksView = browserSettings.viewType === 'tasks' const { isIOS } = useHelpers() - const navItems = (isTasksView: boolean) => [ + const currentNavItems: NavItemType[] = [ { icon: Home, label: t('dashboard'), href: '/', position: 'main' }, { icon: isTasksView ? TaskIcon : HabitIcon, @@ -53,49 +60,12 @@ export default function Navigation({ className, viewPort }: NavigationProps) { }, []) if (viewPort === 'mobile' && isMobileView) { - return ( - <> -
{/* Add padding at the bottom to prevent content from being hidden */} - - - ) + return } if (viewPort === 'main' && !isMobileView) { - return ( -
-
-
-
- -
-
-
-
- ) + return } + + return null // Explicitly return null if no view matches } diff --git a/package.json b/package.json index 17c9451..0e96f11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "habittrove", - "version": "0.2.17", + "version": "0.2.18", "private": true, "scripts": { "dev": "next dev --turbopack",