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 83dbc93..f7ac3e7 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,15 +1,16 @@ import { JotaiHydrate } from '@/components/jotai-hydrate' import { JotaiProvider } from '@/components/jotai-providers' import Layout from '@/components/Layout' +import LoadingSpinner from '@/components/LoadingSpinner' import { ThemeProvider } from "@/components/theme-provider" import { Toaster } from '@/components/ui/toaster' import { SessionProvider } from 'next-auth/react' +import { NextIntlClientProvider } from 'next-intl' +import { getLocale, getMessages } from 'next-intl/server' import { DM_Sans } from 'next/font/google' import { Suspense } from 'react' import { loadCoinsData, loadHabitsData, loadServerSettings, loadSettings, loadUsersData, loadWishlistData } from './actions/data' import './globals.css' -import { NextIntlClientProvider } from 'next-intl'; -import { getLocale, getMessages } from 'next-intl/server'; // Inter (clean, modern, excellent readability) // const inter = Inter({ @@ -73,7 +74,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, setUserSelect]) + if (!isMounted) { + return + } return ( <> {children} diff --git a/components/DesktopNavDisplay.tsx b/components/DesktopNavDisplay.tsx new file mode 100644 index 0000000..20e8b37 --- /dev/null +++ b/components/DesktopNavDisplay.tsx @@ -0,0 +1,33 @@ +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { NavDisplayProps } from './Navigation'; + +export default function DesktopNavDisplay({ navItems }: NavDisplayProps) { + const pathname = usePathname(); + + return ( +
+
+
+
+ +
+
+
+
+ ); +} diff --git a/components/Header.tsx b/components/Header.tsx index 6b6caff..5130e89 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,25 +1,13 @@ -'use client' - -import { FormattedNumber } from '@/components/FormattedNumber' import { Logo } from '@/components/Logo' -import { useCoins } from '@/hooks/useCoins' -import { settingsAtom } from '@/lib/atoms' -import { useAtom } from 'jotai' -import { Coins } from 'lucide-react' -import dynamic from 'next/dynamic' import Link from 'next/link' -import NotificationBell from './NotificationBell' -import { Profile } from './Profile' +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 { balance } = useCoins() return ( <>
@@ -28,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/Layout.tsx b/components/Layout.tsx index 668ab73..ae73a28 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -7,7 +7,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
- +
{/* responsive container (optimized for mobile) */} @@ -17,7 +17,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
- +
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..92e23ac --- /dev/null +++ b/components/MobileNavDisplay.tsx @@ -0,0 +1,33 @@ +import Link from 'next/link' +import { NavDisplayProps, NavItemType } from './Navigation'; +import { usePathname } from 'next/navigation'; +import { useHelpers } from '@/lib/client-helpers'; + +export default function MobileNavDisplay({ navItems }: NavDisplayProps) { + const pathname = usePathname(); + const { isIOS } = useHelpers() + + return ( + <> +
+ + + ); +} diff --git a/components/Navigation.tsx b/components/Navigation.tsx index f90f6dd..622c244 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -1,33 +1,39 @@ 'use client' -import { useHelpers } from '@/lib/client-helpers' import { HabitIcon, TaskIcon } from '@/lib/constants' import { Calendar, Coins, Gift, Home } from 'lucide-react' import { useTranslations } from 'next-intl' -import Link from 'next/link' -import { usePathname } from 'next/navigation' -import { useEffect, useState } from 'react' +import { ElementType, useEffect, useState } from 'react' +import DesktopNavDisplay from './DesktopNavDisplay' +import MobileNavDisplay from './MobileNavDisplay' type ViewPort = 'main' | 'mobile' -interface NavigationProps { - viewPort: ViewPort +export interface NavItemType { + icon: ElementType; + label: string; + href: string; } -export default function Navigation({ viewPort }: NavigationProps) { - const t = useTranslations('Navigation') - const [showAbout, setShowAbout] = useState(false) - const [isMobileView, setIsMobileView] = useState(false) - const { isIOS } = useHelpers() - const pathname = usePathname(); +interface NavigationProps { + position: ViewPort +} - const navItems = () => [ - { icon: Home, label: t('dashboard'), href: '/', position: 'main' }, - { icon: HabitIcon, label: t('habits'), href: '/habits', position: 'main' }, - { icon: TaskIcon, label: t('tasks'), href: '/tasks', position: 'main' }, - { icon: Calendar, label: t('calendar'), href: '/calendar', position: 'main' }, - { icon: Gift, label: t('wishlist'), href: '/wishlist', position: 'main' }, - { icon: Coins, label: t('coins'), href: '/coins', position: 'main' }, +export interface NavDisplayProps { + navItems: NavItemType[]; +} + +export default function Navigation({ position: viewPort }: NavigationProps) { + const t = useTranslations('Navigation') + const [isMobileView, setIsMobileView] = useState(false) + + const currentNavItems: NavItemType[] = [ + { icon: Home, label: t('dashboard'), href: '/' }, + { icon: HabitIcon, label: t('habits'), href: '/habits' }, + { icon: TaskIcon, label: t('tasks'), href: '/tasks' }, + { icon: Calendar, label: t('calendar'), href: '/calendar' }, + { icon: Gift, label: t('wishlist'), href: '/wishlist' }, + { icon: Coins, label: t('coins'), href: '/coins' }, ] useEffect(() => { @@ -46,56 +52,12 @@ export default function Navigation({ 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 3830fb6..4596569 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",