diff --git a/app/[...slug]/loading.tsx b/app/[...slug]/loading.tsx new file mode 100644 index 00000000..f5fbfcf9 --- /dev/null +++ b/app/[...slug]/loading.tsx @@ -0,0 +1,3 @@ +export default function Loading() { + return
loading...
+} diff --git a/app/[...slug]/page.tsx b/app/[...slug]/page.tsx new file mode 100644 index 00000000..7eb661cd --- /dev/null +++ b/app/[...slug]/page.tsx @@ -0,0 +1,71 @@ +import type { Metadata } from "next" +import { notFound } from "next/navigation" +import { App } from "@/components/App" +import { PageContent } from "@/components/PageContent" +import { MetaBuilder, buildMetadata } from "@/lib/meta" +import { getMenu, getPageBySlug, getUrls } from "@/remote/api" +import { tail } from "@/lib/array" + +export const revalidate = 30 +export const dynamicParams = true + +export async function generateStaticParams() { + const urls = await getUrls() + return urls.map(path => ({ + slug: tail(path.split("/")), + })) +} + +type Props = { + params: Promise<{ slug: string[] }> +} + +export async function generateMetadata({ params }: Props): Promise { + const { slug } = await params + const slugPath = `/${slug.join("/")}` + const page = await getPageBySlug(slugPath) + if (!page) { + return {} + } + + const m = new MetaBuilder() + .setImage(page.cover) + .setTitle(page.title) + .setDescription(page.description ?? undefined) + .build() + return buildMetadata(m) +} + +export default async function SlugPage({ params }: Props) { + const { slug } = await params + const slugPath = `/${slug.join("/")}` + const page = await getPageBySlug(slugPath) + if (!page) { + notFound() + } + + const breadcrumbSize = page.breadcrumb?.length ?? 0 + const breadcrumbs = breadcrumbSize < 2 ? [] : page.breadcrumb ?? [] + + return ( + + + + ) +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 00000000..6cc334cc --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,81 @@ +import "src/style/style.css" +import "src/style/content.css" +import "src/style/article.css" +import "src/style/marker/marker-table.css" +import "src/style/kazimir/video.css" + +import type { Metadata, Viewport } from "next" +import Script from "next/script" +import { Roboto } from "next/font/google" +import { Providers } from "@/components/Providers" +import { YMetrika } from "@/components/YMetrika" +import { getMenu } from "@/remote/api" + +const roboto = Roboto({ + weight: ["300", "900"], + subsets: ["cyrillic", "latin"], + display: "swap", +}) + +export const metadata: Metadata = { + verification: { + google: [ + "8BxOTw-Q02DS2KAdcGcnIMwiPZ58DVB18XpOfRibCcg", + "A-TEemUw4hHpULUDCd6xhxfHnyRbgKtss90JGKXnSpw", + ], + other: { + "yandex-verification": "643c7dac144b9dac", + "msvalidate.01": "E878301FE3E3F12D233A6B1156889601", + "proculture-verification": "79110940fd847c3cfe52392ed68f3210", + }, + }, + icons: { + icon: { url: "/static/graphics/favicon-32.png", type: "image/png" }, + apple: [ + { url: "/static/graphics/favicon-57.png", sizes: "57x57" }, + { url: "/static/graphics/favicon-72.png", sizes: "72x72" }, + { url: "/static/graphics/favicon-114.png", sizes: "114x114" }, + { url: "/static/graphics/favicon-144.png", sizes: "144x144" }, + ], + }, + other: { + "apple-mobile-web-app-capable": "yes", + "referrer": "unsafe-url", + }, + alternates: { + types: { + "application/feed+json": "/feed.json", + }, + }, +} + +export const viewport: Viewport = { + width: "device-width", + maximumScale: 1, + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "white" }, + { media: "(prefers-color-scheme: dark)", color: "hsl(240, 10%, 10%)" }, + ], +} + +export const revalidate = 30 + +export default async function RootLayout({ children }: { children: React.ReactNode }) { + const menu = await getMenu() + + return ( + + + {process.env.NEXT_PUBLIC_YMETRIKA_ACCOUNT && ( + + )} + + + + {children} + + - - - - ) -} diff --git a/pages/_document.tsx b/pages/_document.tsx deleted file mode 100644 index a9f85883..00000000 --- a/pages/_document.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Html, Head, Main, NextScript } from "next/document" - -export default function Document() { - return ( - - - {/* Google Fonts */} - - - - -
- - - - ) -} diff --git a/pages/index.tsx b/pages/index.tsx deleted file mode 100644 index b27803c4..00000000 --- a/pages/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import Head from "next/head" -import { App } from "@/components/App" -import { generateNextSeo } from "next-seo/pages" -import { PageGrid } from "@/components/PageGrid" -import { HudozkaTitle } from "@/components/HudozkaTitle" -import { MetaBuilder } from "@/lib/meta" -import type { GetStaticProps, NextPage } from "next" -import type { MenuItem, Meta, PageCardDto } from "@/types" -import { useMobile } from "@/hooks/useMobile" -import { getHomeCards, getMenu } from "@/remote/api" - -type Props = { - title: string - meta: Meta - menu: MenuItem[] - items: PageCardDto[] -} - -const Index: NextPage = props => { - const mobile = useMobile() - - return ( - <> - - {generateNextSeo({ - title: props.meta.title, - description: props.meta.description, - canonical: "https://art.shlisselburg.org/", - openGraph: { - url: props.meta.url, - title: props.meta.title, - description: props.meta.description, - images: [ - { - url: props.meta.image, - width: props.meta.imageWidth, - height: props.meta.imageHeight, - alt: props.meta.description, - type: "image/jpeg", - }, - ], - siteName: props.meta.siteName, - }, - })} - {props.title} - - - {mobile ? null : ( - - )} - - - - - ) -} - -export const getStaticProps: GetStaticProps = async () => { - const items = await getHomeCards() - const menu = await getMenu() - - const title = "Шлиссельбургская ДХШ" - const meta = (new MetaBuilder()) - .setTitle(title) - .setData({ - url: "/", - }) - .build() - - return { - props: { - items, - title, - meta, - menu, - }, - revalidate: 30, - } -} - -export default Index diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index e3843644..54697463 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -1,12 +1,15 @@ +"use client" + import s from "./app.module.css" +import { useContext } from "react" import { Wrapper } from "../Wrapper" import { MenuContext } from "@/context/MenuContext" import { Footer } from "../Footer" import { Content } from "../Content" import { Breadcrumbs } from "../Breadcrumbs" -import type { BreadcrumbPart, MenuItem } from "@/types" -import { useRouter } from "next/router" +import type { BreadcrumbPart } from "@/types" +import { usePathname } from "next/navigation" import { useMobile } from "@/hooks/useMobile" import { Navigation } from "@/components/Navigation" import { Box } from "@/ui/Box" @@ -24,7 +27,7 @@ import Link from "next/link" import AccessibilityPanel from "../AccessibilityPanel" import useAccessibility from "@/hooks/useAccessibility" -const Gosuslugi = dynamic(import("../Gosuslugi").then(module => module.Gosuslugi), { +const Gosuslugi = dynamic(() => import("../Gosuslugi").then(module => ({ default: module.Gosuslugi })), { ssr: false, }) @@ -32,103 +35,101 @@ export type AppProps = { showAuthor?: boolean contentStyle?: React.CSSProperties breadcrumbs?: BreadcrumbPart[] - menu: MenuItem[] children?: React.ReactNode } -export function App({ showAuthor = false, menu, ...props }: AppProps) { +export function App({ showAuthor = false, ...props }: AppProps) { const hideBreadcrumbs = useMobile() const accessibility = useAccessibility() - const router = useRouter() + const pathname = usePathname() + const menu = useContext(MenuContext) return ( - - - {!accessibility ? null : ( - - )} + + {!accessibility ? null : ( + + )} -
- -
- {hideBreadcrumbs || !props.breadcrumbs ? null : ( - - - - - - )} - - )} - footer={( -
- - - - - - - - Версия для слабовидящих - - - - - - - Анкеты - - -
    -
  • - - Независимая оценка качества оказания услуг - -
  • - -
  • - - Анкета Министерства культуры РФ - -
  • - -
  • - - Анкета получателей образовательных услуг ДОП Кировского муниципального района Ленинградской области 2025 - -
  • -
-
+
+ +
+ {hideBreadcrumbs || !props.breadcrumbs ? null : ( + + + + + )} + + )} + footer={( +
+ + + + -
- -
+ + + Версия для слабовидящих + + + - - + + + Анкеты + - {!showAuthor ? null : ( - <> - - - - )} - -
- )} - > - - - {props.children} - - - - +
    +
  • + + Независимая оценка качества оказания услуг + +
  • + +
  • + + Анкета Министерства культуры РФ + +
  • + +
  • + + Анкета получателей образовательных услуг ДОП Кировского муниципального района Ленинградской области 2025 + +
  • +
+ +
+ +
+ +
+ + + + + {!showAuthor ? null : ( + <> + + + + )} + +
+ )} + > + + + {props.children} + + +
) } diff --git a/src/components/FileCard/index.tsx b/src/components/FileCard/index.tsx index 35a1e566..a7302a03 100644 --- a/src/components/FileCard/index.tsx +++ b/src/components/FileCard/index.tsx @@ -8,7 +8,7 @@ import { getResizedUrl } from "@/lib/image" import Link from "next/link" import { Box } from "@/ui/Box" -const Signature = dynamic(import("./Signature").then(m => m.Signature), { +const Signature = dynamic(() => import("./Signature").then(m => ({ default: m.Signature })), { ssr: false, }) diff --git a/src/components/HomeContent/index.tsx b/src/components/HomeContent/index.tsx new file mode 100644 index 00000000..70a67c2f --- /dev/null +++ b/src/components/HomeContent/index.tsx @@ -0,0 +1,29 @@ +"use client" + +import { HudozkaTitle } from "@/components/HudozkaTitle" +import { PageGrid } from "@/components/PageGrid" +import { useMobile } from "@/hooks/useMobile" +import type { PageCardDto } from "@/types" + +export type HomeContentProps = { + items: PageCardDto[] +} + +export function HomeContent({ items }: HomeContentProps) { + const mobile = useMobile() + + return ( + <> + {mobile ? null : ( + + )} + + + + ) +} diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index 5ab8017a..225b5c82 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -1,9 +1,11 @@ +"use client" + import s from "./styles.module.css" import type { MenuItem } from "@/types" import * as NavigationMenu from "@radix-ui/react-navigation-menu" import classnames from "classnames/bind" -import { useRouter } from "next/router" +import { usePathname } from "next/navigation" import { ActiveLink } from "./ActiveLink" let cx = classnames.bind(s) @@ -15,13 +17,13 @@ export type MenuProps = { } export const Menu: React.FC = ({ vertical = false, onItemClick, items }) => { - const router = useRouter() + const pathname = usePathname() return ( { items.map(item => { - const current = router.asPath === item.href + const current = pathname === item.href const href = current ? undefined : item.href diff --git a/src/components/Navigation/MobileNavigation.tsx b/src/components/Navigation/MobileNavigation.tsx index 40e7e6ac..8d3d9d9b 100644 --- a/src/components/Navigation/MobileNavigation.tsx +++ b/src/components/Navigation/MobileNavigation.tsx @@ -1,3 +1,5 @@ +"use client" + import s from "./nav.module.css" import { Menu } from "../Menu" @@ -8,7 +10,7 @@ import { Spacer } from "../Spacer" import HudozkaLogo from "../HudozkaLogo" import { HudozkaTitle } from "../HudozkaTitle" import Link from "next/link" -import { useRouter } from "next/router" +import { usePathname } from "next/navigation" import { HiOutlineMenuAlt4, HiOutlineX } from "react-icons/hi" export type MobileNavigationProps = { @@ -19,7 +21,7 @@ export type MobileNavigationProps = { export function MobileNavigation({ className, style }: MobileNavigationProps) { const menu = useContext(MenuContext) const detailsRef = useRef(null) - const router = useRouter() + const pathname = usePathname() const close = useCallback(() => { if (detailsRef.current) { @@ -28,11 +30,8 @@ export function MobileNavigation({ className, style }: MobileNavigationProps) { }, []) useEffect(() => { - router.events.on("routeChangeStart", close) - return () => { - router.events.off("routeChangeStart", close) - } - }, [router.events, close]) + close() + }, [pathname, close]) return (