diff --git a/src/webapp/app/(authenticated)/layout.tsx b/src/webapp/app/(authenticated)/layout.tsx index 7a9bae53..c0b988e4 100644 --- a/src/webapp/app/(authenticated)/layout.tsx +++ b/src/webapp/app/(authenticated)/layout.tsx @@ -1,37 +1,21 @@ 'use client'; import { useEffect } from 'react'; -import { usePathname, useRouter } from 'next/navigation'; +import { useRouter } from 'next/navigation'; import { useAuth } from '@/hooks/useAuth'; -import { useDisclosure, useMediaQuery } from '@mantine/hooks'; -import { - ActionIcon, - AppShell, - Group, - NavLink, - Text, - Affix, -} from '@mantine/core'; -import { FiSidebar } from 'react-icons/fi'; -import { IoDocumentTextOutline } from 'react-icons/io5'; -import { BsFolder2 } from 'react-icons/bs'; -import { BiUser } from 'react-icons/bi'; +import { useDisclosure } from '@mantine/hooks'; +import { ActionIcon, AppShell, Affix } from '@mantine/core'; import { FiPlus } from 'react-icons/fi'; -import { HiOutlineGlobeAlt } from 'react-icons/hi'; +import Header from '@/components/navigation/header/Header'; +import Navbar from '@/components/navigation/navbar/Navbar'; -export default function AuthenticatedLayout({ - children, -}: { +interface Props { children: React.ReactNode; -}) { - const { isAuthenticated, isLoading } = useAuth(); +} +export default function AuthenticatedLayout(props: Props) { const router = useRouter(); - - const [mobileOpened, { toggle: toggleMobile }] = useDisclosure(); - const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true); - const isMobile = useMediaQuery('(max-width: 768px)'); - - const pathname = usePathname(); + const { isAuthenticated, isLoading } = useAuth(); + const [opened, { toggle }] = useDisclosure(); useEffect(() => { if (!isLoading && !isAuthenticated) { @@ -49,67 +33,23 @@ export default function AuthenticatedLayout({ navbar={{ width: 300, breakpoint: 'sm', - collapsed: { mobile: !mobileOpened, desktop: !desktopOpened }, + collapsed: { mobile: !opened, desktop: opened }, }} padding="md" > - - - { - isMobile ? toggleMobile() : toggleDesktop(); - }} - > - - - Semble - - - - - } - /> - } - /> - } - /> - } - mt="auto" - /> - +
+ - {children} + {props.children} router.push('/cards/add')} - size={56} + size={'input-lg'} radius="xl" - color="blue" variant="filled" - style={{ - boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', - }} > - + diff --git a/src/webapp/app/layout.tsx b/src/webapp/app/layout.tsx index 5b7a911b..699c8f53 100644 --- a/src/webapp/app/layout.tsx +++ b/src/webapp/app/layout.tsx @@ -1,14 +1,8 @@ import { Analytics } from '@vercel/analytics/next'; import type { Metadata } from 'next'; -import { - ColorSchemeScript, - mantineHtmlProps, - MantineProvider, -} from '@mantine/core'; -import '@mantine/core/styles.css'; -import { AuthProvider } from '@/hooks/useAuth'; -import { theme } from '@/styles/theme'; +import { ColorSchemeScript, mantineHtmlProps } from '@mantine/core'; import { Hanken_Grotesk } from 'next/font/google'; +import Providers from '@/providers'; export const metadata: Metadata = { title: { @@ -37,9 +31,7 @@ export default function RootLayout({ - - {children} - + {children} diff --git a/src/webapp/components/navigation/header/Header.tsx b/src/webapp/components/navigation/header/Header.tsx new file mode 100644 index 00000000..3e2ac0d6 --- /dev/null +++ b/src/webapp/components/navigation/header/Header.tsx @@ -0,0 +1,34 @@ +import { ActionIcon, AppShellHeader, Group, Image } from '@mantine/core'; +import SembleLogo from '@/assets/semble-logo.svg'; +import { FiSidebar } from 'react-icons/fi'; + +interface Props { + onToggleNavbar: () => void; +} + +export default function Header(props: Props) { + return ( + + + + Semble logo + + + + + + + ); +} diff --git a/src/webapp/components/navigation/navItem/NavItem.tsx b/src/webapp/components/navigation/navItem/NavItem.tsx new file mode 100644 index 00000000..5a0c464e --- /dev/null +++ b/src/webapp/components/navigation/navItem/NavItem.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { NavLink } from '@mantine/core'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +interface Props { + href: string; + label: string; + icon: React.ReactElement; + activeIcon: React.ReactElement; + badge?: number; +} + +export default function NavItem(props: Props) { + const pathname = usePathname(); + const isActive = pathname === props.href; + + return ( + + ); +} diff --git a/src/webapp/components/navigation/navbar/Navbar.tsx b/src/webapp/components/navigation/navbar/Navbar.tsx new file mode 100644 index 00000000..f2eda70f --- /dev/null +++ b/src/webapp/components/navigation/navbar/Navbar.tsx @@ -0,0 +1,43 @@ +import NavItem from '../navItem/NavItem'; +import { + AppShellSection, + AppShellNavbar, + ScrollArea, + Divider, +} from '@mantine/core'; +import { IoDocumentTextOutline } from 'react-icons/io5'; +import { MdOutlineEmojiNature } from 'react-icons/md'; + +export default function Navbar() { + return ( + + + } + activeIcon={} + /> + } + activeIcon={} + /> + + + {/* + + */} + + + + + ); +} diff --git a/src/webapp/package-lock.json b/src/webapp/package-lock.json index c07e7d9b..2dab6d60 100644 --- a/src/webapp/package-lock.json +++ b/src/webapp/package-lock.json @@ -14,6 +14,7 @@ "@mantine/form": "^8.1.3", "@mantine/hooks": "^8.1.3", "@mantine/notifications": "^8.1.3", + "@tanstack/react-query": "^5.85.5", "@vercel/analytics": "^1.5.0", "date-fns": "^4.1.0", "dayjs": "^1.11.13", @@ -7075,6 +7076,30 @@ "node": ">=14.16" } }, + "node_modules/@tanstack/query-core": { + "version": "5.85.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.5.tgz", + "integrity": "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.85.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.5.tgz", + "integrity": "sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A==", + "dependencies": { + "@tanstack/query-core": "5.85.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", diff --git a/src/webapp/package.json b/src/webapp/package.json index 154aa1a3..8b960a40 100644 --- a/src/webapp/package.json +++ b/src/webapp/package.json @@ -30,6 +30,7 @@ "@mantine/form": "^8.1.3", "@mantine/hooks": "^8.1.3", "@mantine/notifications": "^8.1.3", + "@tanstack/react-query": "^5.85.5", "@vercel/analytics": "^1.5.0", "date-fns": "^4.1.0", "dayjs": "^1.11.13", diff --git a/src/webapp/providers/index.tsx b/src/webapp/providers/index.tsx new file mode 100644 index 00000000..88c8f5ce --- /dev/null +++ b/src/webapp/providers/index.tsx @@ -0,0 +1,19 @@ +'use client'; + +import { AuthProvider } from '@/hooks/useAuth'; +import MantineProvider from './mantine'; +import TanStackQueryProvider from './tanstack'; + +interface Props { + children: React.ReactNode; +} + +export default function Providers(props: Props) { + return ( + + + {props.children} + + + ); +} diff --git a/src/webapp/providers/mantine.tsx b/src/webapp/providers/mantine.tsx new file mode 100644 index 00000000..fcefc633 --- /dev/null +++ b/src/webapp/providers/mantine.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { theme } from '@/styles/theme'; +import { MantineProvider as BaseProvider } from '@mantine/core'; +import '@mantine/core/styles.css'; + +interface Props { + children: React.ReactNode; +} + +export default function MantineProvider(props: Props) { + return {props.children}; +} diff --git a/src/webapp/providers/tanstack.tsx b/src/webapp/providers/tanstack.tsx new file mode 100644 index 00000000..287968a1 --- /dev/null +++ b/src/webapp/providers/tanstack.tsx @@ -0,0 +1,53 @@ +'use client'; + +import { + QueryClient, + QueryClientProvider, + defaultShouldDehydrateQuery, + isServer, +} from '@tanstack/react-query'; + +function makeQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, + }, + dehydrate: { + // include pending queries in dehydration + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || + query.state.status === 'pending', + }, + }, + }); +} + +let browserQueryClient: QueryClient | undefined = undefined; + +function getQueryClient() { + if (isServer) { + // Server: always make a new query client + return makeQueryClient(); + } else { + // Browser: make a new query client if we don't already have one + // This is very important, so we don't re-make a new client if React + // suspends during the initial render. This may not be needed if we + // have a suspense boundary BELOW the creation of the query client + if (!browserQueryClient) browserQueryClient = makeQueryClient(); + return browserQueryClient; + } +} + +interface Props { + children: React.ReactNode; +} +export default function TanStackQueryProvider(props: Props) { + const queryClient = getQueryClient(); + + return ( + + {props.children} + + ); +} diff --git a/src/webapp/styles/theme.tsx b/src/webapp/styles/theme.tsx index 15ab49f1..093581eb 100644 --- a/src/webapp/styles/theme.tsx +++ b/src/webapp/styles/theme.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Button, createTheme, TextInput } from '@mantine/core'; +import { Button, createTheme, NavLink, TextInput } from '@mantine/core'; export const theme = createTheme({ primaryColor: 'tangerine', @@ -51,5 +51,13 @@ export const theme = createTheme({ radius: 'xl', }, }), + NavLink: NavLink.extend({ + styles: (theme) => ({ + label: { + fontSize: theme.fontSizes.md, + fontWeight: 600, + }, + }), + }), }, });