diff --git a/browser/create-template/templates/nextjs-site/next.config.mjs b/browser/create-template/templates/nextjs-site/next.config.mjs index db0ab9c7..910618b1 100644 --- a/browser/create-template/templates/nextjs-site/next.config.mjs +++ b/browser/create-template/templates/nextjs-site/next.config.mjs @@ -1,9 +1,5 @@ -import path from 'path'; - /** @type {import('next').NextConfig} */ const nextConfig = { - // temporary workaround for using symlinked package with turbopack - outputFileTracingRoot: path.join(import.meta.dirname, '../../../'), webpack: config => { config.resolve.extensionAlias = { '.js': ['.ts', '.tsx', '.js'], diff --git a/browser/create-template/templates/nextjs-site/src/app/[[...slug]]/page.tsx b/browser/create-template/templates/nextjs-site/src/app/[[...slug]]/page.tsx index 896442b2..8514731f 100644 --- a/browser/create-template/templates/nextjs-site/src/app/[[...slug]]/page.tsx +++ b/browser/create-template/templates/nextjs-site/src/app/[[...slug]]/page.tsx @@ -4,8 +4,6 @@ import { core } from '@tomic/lib'; import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; -export let currentSubject: string | undefined; - type Params = { slug?: string[]; }; @@ -15,7 +13,7 @@ type Props = { searchParams?: Promise>; }; -export const fetchResource = async (slug?: string[]) => { +const fetchResource = async (slug?: string[]) => { const path = slug ? `/${slug.join('/')}` : '/'; return await getCurrentResource(path); }; @@ -41,8 +39,6 @@ const Page = async ({ params, searchParams }: Props) => { return notFound(); } - currentSubject = resource.subject; - return ; }; diff --git a/browser/create-template/templates/nextjs-site/src/app/context/CurrentSubjectContext.tsx b/browser/create-template/templates/nextjs-site/src/app/context/CurrentSubjectContext.tsx deleted file mode 100644 index 64a1e1da..00000000 --- a/browser/create-template/templates/nextjs-site/src/app/context/CurrentSubjectContext.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; - -import { env } from '@/env'; -import { createContext, useContext, useState } from 'react'; - -interface CurrentSubjectContextType { - currentSubject: string; - setCurrentSubject: (newSubject: string) => void; -} - -const CurrentSubjectContext = createContext< - CurrentSubjectContextType | undefined ->(undefined); - -export const CurrentSubjectProvider = ({ - children, -}: { - children: React.ReactNode; -}) => { - const [currentSubject, setCurrentSubject] = useState( - env.NEXT_PUBLIC_WEBSITE_RESOURCE, - ); - return ( - - {children} - - ); -}; - -export const useCurrentSubject = () => { - const context = useContext(CurrentSubjectContext); - if (!context) { - throw new Error( - 'useCurrentSubject must be used within a CurrentSubjectProvider', - ); - } - return context; -}; diff --git a/browser/create-template/templates/nextjs-site/src/app/context/CurrentSubjectProvider.tsx b/browser/create-template/templates/nextjs-site/src/app/context/CurrentSubjectProvider.tsx new file mode 100644 index 00000000..80811a14 --- /dev/null +++ b/browser/create-template/templates/nextjs-site/src/app/context/CurrentSubjectProvider.tsx @@ -0,0 +1,48 @@ +import React, { + createContext, + SetStateAction, + Dispatch, + PropsWithChildren, + useState, + useContext, + useEffect, +} from 'react'; + +const CurrentSubjectContext = createContext<{ + currentSubject: string; + setCurrentSubject: Dispatch>; +}>({ + currentSubject: '', + setCurrentSubject: () => '', +}); + +export const CurrentSubjectProvider = ({ + currentSubject: initialSubject, + children, +}: PropsWithChildren<{ currentSubject: string }>) => { + const [currentSubject, setCurrentSubject] = useState(initialSubject); + + useEffect(() => { + setCurrentSubject(initialSubject); + }, [initialSubject]); + + return ( + + {children} + + ); +}; + +export const useCurrentSubject = () => { + const context = useContext(CurrentSubjectContext); + + if (!context) { + throw new Error( + 'useCurrentSubject must be used within a CurrentSubjectProvider', + ); + } + + return context; +}; diff --git a/browser/create-template/templates/nextjs-site/src/app/layout.tsx b/browser/create-template/templates/nextjs-site/src/app/layout.tsx index 2b52c197..e373fcf6 100644 --- a/browser/create-template/templates/nextjs-site/src/app/layout.tsx +++ b/browser/create-template/templates/nextjs-site/src/app/layout.tsx @@ -12,9 +12,7 @@ export const metadata: Metadata = { description: 'A Next.js template for Atomic Server', }; -export const dynamic = 'force-dynamic'; - -export default function RootLayout({ +export default async function RootLayout({ children, }: { children: React.ReactNode; diff --git a/browser/create-template/templates/nextjs-site/src/components/Navbar.tsx b/browser/create-template/templates/nextjs-site/src/components/Navbar.tsx index 081973e3..e506930b 100644 --- a/browser/create-template/templates/nextjs-site/src/components/Navbar.tsx +++ b/browser/create-template/templates/nextjs-site/src/components/Navbar.tsx @@ -1,7 +1,7 @@ import Container from './Layout/Container'; import HStack from './Layout/HStack'; import { env } from '@/env'; -import { Website } from '@/ontologies/website'; +import type { Website } from '@/ontologies/website'; import MenuItem from '@/views/MenuItem/MenuItem'; import styles from './Navbar.module.css'; import { store } from '@/store'; diff --git a/browser/create-template/templates/nextjs-site/src/components/ProviderWrapper.tsx b/browser/create-template/templates/nextjs-site/src/components/ProviderWrapper.tsx index 81d19f10..aeb98b60 100644 --- a/browser/create-template/templates/nextjs-site/src/components/ProviderWrapper.tsx +++ b/browser/create-template/templates/nextjs-site/src/components/ProviderWrapper.tsx @@ -1,18 +1,31 @@ 'use client'; import { StoreContext } from '@tomic/react'; -import { CurrentSubjectProvider } from '@/app/context/CurrentSubjectContext'; +import { CurrentSubjectProvider } from '@/app/context/CurrentSubjectProvider'; import { store } from '@/store'; import { initOntologies } from '@/ontologies'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { usePathname } from 'next/navigation'; +import { getCurrentResource } from '@/atomic/getCurrentResource'; const ProviderWrapper = ({ children }: { children: React.ReactNode }) => { // Registers your ontologies with the store initOntologies(); + const pathname = usePathname(); + const [currentSubject, setCurrentSubject] = useState(''); + + useEffect(() => { + getCurrentResource(pathname).then(resource => { + setCurrentSubject(resource?.subject ?? ''); + }); + }, [pathname]); + return ( - {children} + + {children} + ); }; diff --git a/browser/create-template/templates/nextjs-site/src/views/FullPage/BlogpostFullPage.tsx b/browser/create-template/templates/nextjs-site/src/views/FullPage/BlogpostFullPage.tsx index 73a3174a..9b1cdb20 100644 --- a/browser/create-template/templates/nextjs-site/src/views/FullPage/BlogpostFullPage.tsx +++ b/browser/create-template/templates/nextjs-site/src/views/FullPage/BlogpostFullPage.tsx @@ -24,7 +24,9 @@ const BlogpostFullPage = ({ resource }: { resource: Resource }) => { return (
- +
+ +

{resource.title}

{date}

diff --git a/browser/create-template/templates/nextjs-site/src/views/MenuItem/MenuItem.module.css b/browser/create-template/templates/nextjs-site/src/views/MenuItem/MenuItem.module.css index 78784394..1a22521a 100644 --- a/browser/create-template/templates/nextjs-site/src/views/MenuItem/MenuItem.module.css +++ b/browser/create-template/templates/nextjs-site/src/views/MenuItem/MenuItem.module.css @@ -4,6 +4,7 @@ } .button { + anchor-name: var(--anchor-name); padding: 0.4rem; display: inline-flex; align-items: center; @@ -14,7 +15,6 @@ background: none; cursor: pointer; transition: background-color 100ms ease-in-out; - anchor-name: --menu-item-anchor; &:hover, &:focus-visible { background-color: var(--theme-color-bg-2); @@ -22,10 +22,8 @@ } .submenu { - position-anchor: --menu-item-anchor; - inset-area: bottom center; - position-area: bottom center; - width: max(20ch, anchor-size(width)); + --menu-width: 20ch; + width: var(--menu-width); border: 1px solid var(--theme-color-bg-1); border-radius: var(--theme-border-radius); box-shadow: @@ -35,4 +33,14 @@ 0px 22.3px 17.9px rgba(0, 0, 0, 0.042), 0px 41.8px 33.4px rgba(0, 0, 0, 0.05), 0px 100px 80px rgba(0, 0, 0, 0.07); + + position-anchor: var(--anchor-name); + position-area: bottom center; + + @supports not (anchor-name: --something) { + --supports-position-anchor: false; + position: fixed; + top: var(--top); + left: var(--left); + } } diff --git a/browser/create-template/templates/nextjs-site/src/views/MenuItem/MenuItem.tsx b/browser/create-template/templates/nextjs-site/src/views/MenuItem/MenuItem.tsx index c72c7832..c9c69491 100644 --- a/browser/create-template/templates/nextjs-site/src/views/MenuItem/MenuItem.tsx +++ b/browser/create-template/templates/nextjs-site/src/views/MenuItem/MenuItem.tsx @@ -1,15 +1,55 @@ +'use client'; + import type { MenuItem } from '@/ontologies/website'; import MenuItemLink from './MenuItemLink'; import styles from './MenuItem.module.css'; -import { store } from '@/store'; -import { useId } from 'react'; -import { currentSubject } from '@/app/[[...slug]]/page'; // BAD 👎 +import { useResource } from '@tomic/react'; +import { useCurrentSubject } from '@/app/context/CurrentSubjectProvider'; +import { useId, useRef, useState } from 'react'; -const MenuItem = async ({ subject }: { subject: string }) => { +const MenuItem = ({ subject }: { subject: string }) => { + const menuItem = useResource(subject); + const { currentSubject } = useCurrentSubject(); const id = useId(); - const anchorName = `--menuItem-${id}`; + const anchorName = CSS.escape(`--menuItem-${id}`); + const popover = useRef(null); + const button = useRef(null); + const [submenuPosition, setSubmenuPosition] = useState({ + top: '0px', + left: '0px', + }); + + const calcPopoverPosition = () => { + if (!button.current || !popover.current) return; + + if (CSS.supports('anchor-name', '--something')) { + return; + } + + const rect = button.current.getBoundingClientRect(); + + const newSubmenuPosition = { ...submenuPosition }; + + newSubmenuPosition.top = `calc(${rect.top}px + 2rem)`; + newSubmenuPosition.left = `calc(${rect.left}px - (var(--menu-width) / 2 - ${ + rect.width / 2 + }px))`; + + setSubmenuPosition(newSubmenuPosition); + }; + + const closePopover = () => { + popover.current?.hidePopover(); + }; - const menuItem = await store.getResource(subject); + const onFocusOut = (event: React.FocusEvent) => { + if ( + !event.relatedTarget || + !event.currentTarget.contains(event.relatedTarget) + ) { + closePopover(); + } + }; return menuItem.props.subItems && menuItem.props.subItems.length > 0 ? ( <> @@ -17,7 +57,10 @@ const MenuItem = async ({ subject }: { subject: string }) => { className={styles.button} popoverTarget={id} popoverTargetAction='toggle' + onClick={calcPopoverPosition} + ref={button} style={{ '--anchor-name': anchorName } as React.CSSProperties} + suppressHydrationWarning > {menuItem.title} @@ -25,12 +68,17 @@ const MenuItem = async ({ subject }: { subject: string }) => {
    {menuItem.props.subItems?.map((subItem: string, index: number) => ( diff --git a/browser/create-template/templates/nextjs-site/src/views/MenuItem/MenuItemLink.tsx b/browser/create-template/templates/nextjs-site/src/views/MenuItem/MenuItemLink.tsx index 5c1f619a..6eb241f4 100644 --- a/browser/create-template/templates/nextjs-site/src/views/MenuItem/MenuItemLink.tsx +++ b/browser/create-template/templates/nextjs-site/src/views/MenuItem/MenuItemLink.tsx @@ -2,20 +2,19 @@ import { website } from '@/ontologies/website'; import { unknownSubject, Resource } from '@tomic/lib'; import styles from './MenuItemLink.module.css'; import clsx from 'clsx'; -import { store } from '@/store'; +import { useResource } from '@tomic/react'; +import Link from 'next/link'; -const MenuItemLink = async ({ +const MenuItemLink = ({ resource, active = false, }: { resource: Resource; active?: boolean; }) => { - const page = await store.getResource(resource.subject ?? unknownSubject); + const page = useResource(resource.subject ?? unknownSubject); - const pageHrefValue = await store.getResource( - page.get(website.properties.linksTo), - ); + const pageHrefValue = useResource(page.get(website.properties.linksTo)); const href = pageHrefValue.get(website.properties.href) ?? @@ -23,13 +22,14 @@ const MenuItemLink = async ({ '#'; return ( - - {resource.title} - + {resource.loading ? '' : page.title} + ); };