diff --git a/SELF_HOSTING.md b/SELF_HOSTING.md index 1bbbc76..2023b2b 100644 --- a/SELF_HOSTING.md +++ b/SELF_HOSTING.md @@ -249,6 +249,15 @@ If startup fails after a schema migration, restore a pre-upgrade backup with the matching older image. Do not automatically downgrade the image over newer data. See `DEPLOYMENT.md` for the simplified Gitea workflow. +## Loading feedback + +Initial rendering and language changes use a full-page indicator. Case-study +opening and next/previous navigation keep the drawer and its close control while +showing a loading indicator inside it. Indicators follow navigation/rendering +state without artificial delays or waiting for off-screen images and 3D scenes. +They follow the current theme, honor reduced motion, and expose localized status +labels under Languages in the editor. + ## Build checks ```sh diff --git a/src/app/(en)/projects/[projectId]/error.tsx b/src/app/(en)/projects/[projectId]/error.tsx new file mode 100644 index 0000000..326ce74 --- /dev/null +++ b/src/app/(en)/projects/[projectId]/error.tsx @@ -0,0 +1,2 @@ +'use client'; +export { PortfolioError as default } from '@/components/PortfolioError'; diff --git a/src/app/(en)/projects/[projectId]/loading.tsx b/src/app/(en)/projects/[projectId]/loading.tsx new file mode 100644 index 0000000..95b8478 --- /dev/null +++ b/src/app/(en)/projects/[projectId]/loading.tsx @@ -0,0 +1 @@ +export { CaseStudyLoading as default } from '@/components/CaseStudyLoading'; diff --git a/src/app/(en)/projects/layout.tsx b/src/app/(en)/projects/layout.tsx index bba52c7..2370c89 100644 --- a/src/app/(en)/projects/layout.tsx +++ b/src/app/(en)/projects/layout.tsx @@ -1,6 +1,3 @@ -import { CaseStudyDrawer } from '@/components/CaseStudyDrawer'; -import { getPublicRoute } from '@/server/public'; -export default async function ProjectsLayout({ children, }: { children: React.ReactNode; }) { - const { portfolio } = await getPublicRoute(); - return {children}; +export default function ProjectsLayout({ children }: { children: React.ReactNode }) { + return children; } diff --git a/src/app/(localized)/[locale]/projects/[projectId]/error.tsx b/src/app/(localized)/[locale]/projects/[projectId]/error.tsx new file mode 100644 index 0000000..326ce74 --- /dev/null +++ b/src/app/(localized)/[locale]/projects/[projectId]/error.tsx @@ -0,0 +1,2 @@ +'use client'; +export { PortfolioError as default } from '@/components/PortfolioError'; diff --git a/src/app/(localized)/[locale]/projects/[projectId]/loading.tsx b/src/app/(localized)/[locale]/projects/[projectId]/loading.tsx new file mode 100644 index 0000000..95b8478 --- /dev/null +++ b/src/app/(localized)/[locale]/projects/[projectId]/loading.tsx @@ -0,0 +1 @@ +export { CaseStudyLoading as default } from '@/components/CaseStudyLoading'; diff --git a/src/app/(localized)/[locale]/projects/layout.tsx b/src/app/(localized)/[locale]/projects/layout.tsx index 29de877..2370c89 100644 --- a/src/app/(localized)/[locale]/projects/layout.tsx +++ b/src/app/(localized)/[locale]/projects/layout.tsx @@ -1,6 +1,3 @@ -import { CaseStudyDrawer } from '@/components/CaseStudyDrawer'; -import { getPublicRoute } from '@/server/public'; -export default async function ProjectsLayout({ children, params }: { children: React.ReactNode; params: Promise<{ locale: string }>; }) { - const { portfolio } = await getPublicRoute([(await params).locale]); - return {children}; +export default function ProjectsLayout({ children }: { children: React.ReactNode }) { + return children; } diff --git a/src/app/PublicLayout.tsx b/src/app/PublicLayout.tsx index 5b92dd5..80eb087 100644 --- a/src/app/PublicLayout.tsx +++ b/src/app/PublicLayout.tsx @@ -1,9 +1,13 @@ import type { Metadata } from 'next'; +import { Suspense } from 'react'; import { SiteRoot } from './SiteRoot'; import { PortfolioPage } from './PortfolioPage'; import { PortfolioRouteShell } from '@/components/PortfolioRouteShell'; -import { getPublicRoute } from '@/server/public'; +import { getPublicLanguage, getPublicRoute } from '@/server/public'; import { portfolioPath } from '@/content/contracts'; +import { resolveMessages } from '@/content/messages'; +import { InitialLoading } from '@/components/InitialLoading'; +import { LoadingIndicator } from '@/components/LoadingIndicator'; import '@/styles/globals.css'; export async function publicMetadata(locale?: string): Promise { @@ -12,10 +16,21 @@ export async function publicMetadata(locale?: string): Promise { return { ...portfolio.metadata, alternates: { canonical, languages: Object.fromEntries(portfolio.languages.map(language => [language.code, portfolioPath(language.code, portfolio.defaultLanguage)])) } }; } export async function PublicLayout({ children, locale }: { children: React.ReactNode; locale?: string }) { - const { portfolio } = await getPublicRoute(locale ? [locale] : []); - return - - {children} - + const language = await getPublicLanguage(locale); + const copy = resolveMessages(language.code, language.messages); + return + }> + {children} + ; } + +async function PublicContent({ children, locale }: { children: React.ReactNode; locale?: string }) { + const { portfolio } = await getPublicRoute(locale ? [locale] : []); + return <> + + }> + {children} + + ; +} diff --git a/src/components/CaseStudyContent.tsx b/src/components/CaseStudyContent.tsx index dd0ab0e..c55e527 100644 --- a/src/components/CaseStudyContent.tsx +++ b/src/components/CaseStudyContent.tsx @@ -1,4 +1,4 @@ -import Link from 'next/link'; +import { PortfolioLink as Link } from './PortfolioNavigation'; import { ArrowLeft, ArrowRight } from 'lucide-react'; import type { PublicPortfolio } from '@/server/content'; import type { Project } from '@/types/projects'; diff --git a/src/components/CaseStudyDrawer.tsx b/src/components/CaseStudyDrawer.tsx index 859ad65..40cfbd0 100644 --- a/src/components/CaseStudyDrawer.tsx +++ b/src/components/CaseStudyDrawer.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useRef, type PointerEvent, type ReactNode } fro import { X } from 'lucide-react'; import { usePathname } from 'next/navigation'; import type { PublicPortfolio } from '@/server/content'; -import { useDismissCaseStudy } from './PortfolioRouteShell'; +import { useDismissCaseStudy } from './PortfolioNavigation'; import { CaseStudyShare } from './CaseStudyShare'; import { useDrawerDrag } from './useDrawerDrag'; import styles from './CaseStudyDrawer.module.css'; diff --git a/src/components/CaseStudyLoading.tsx b/src/components/CaseStudyLoading.tsx new file mode 100644 index 0000000..91d46a6 --- /dev/null +++ b/src/components/CaseStudyLoading.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { LoadingIndicator } from './LoadingIndicator'; +import { usePortfolioNavigation } from './PortfolioNavigation'; +import styles from './LoadingIndicator.module.css'; + +export function CaseStudyLoading() { + const { loadingLabel } = usePortfolioNavigation(); + return <> +

{loadingLabel}

+
+ ; +} diff --git a/src/components/HeroControls.tsx b/src/components/HeroControls.tsx index e0c5754..36fed76 100644 --- a/src/components/HeroControls.tsx +++ b/src/components/HeroControls.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { portfolioPath } from '@/content/contracts'; -import Link from 'next/link'; +import { PortfolioLink as Link } from './PortfolioNavigation'; import Image from 'next/image'; import { languageFlag } from '@/content/language-flags'; import { Check, Globe, Languages, Moon, Sun } from 'lucide-react'; @@ -50,7 +50,7 @@ export function HeroControls({ portfolio }: PortfolioProps) { {portfolio.languages.map(language => { const flag = languageFlag(language.code, language.flagCountry); return {flag ? :