1 parent
0c8d74b21d
commit
a1e51e78f0
23 files changed
+522
-516
No files matched your search
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return <div>loading...</div>
|
||||
}
|
||||
@@ -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<Metadata> {
|
||||
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 (
|
||||
<App
|
||||
contentStyle={{
|
||||
marginTop: "var(--size-l)",
|
||||
marginBottom: "var(--size-xl)",
|
||||
}}
|
||||
breadcrumbs={breadcrumbs}
|
||||
>
|
||||
<PageContent
|
||||
tags={page.tags}
|
||||
date={page.date}
|
||||
tokens={page.tokens}
|
||||
documentSignature={{
|
||||
date: "20.01.2021г.",
|
||||
person: "Тимашева Марина Геннадьевна",
|
||||
position: "Директор",
|
||||
signature: "0ac4ea89753a4ba9893799442325fb41",
|
||||
}}
|
||||
/>
|
||||
</App>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<html lang="ru" className={roboto.className}>
|
||||
<head>
|
||||
{process.env.NEXT_PUBLIC_YMETRIKA_ACCOUNT && (
|
||||
<YMetrika account={Number(process.env.NEXT_PUBLIC_YMETRIKA_ACCOUNT)} />
|
||||
)}
|
||||
</head>
|
||||
<body>
|
||||
<Providers menu={menu}>
|
||||
{children}
|
||||
</Providers>
|
||||
<Script src="https://culturaltracking.ru/static/js/spxl.js?pixelId=27154" data-pixel-id="27154" />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { App } from "@/components/App"
|
||||
import { NotFound } from "@/components/NotFound"
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<App>
|
||||
<NotFound />
|
||||
</App>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Metadata } from "next"
|
||||
import { App } from "@/components/App"
|
||||
import { HomeContent } from "@/components/HomeContent"
|
||||
import { MetaBuilder, buildMetadata } from "@/lib/meta"
|
||||
import { getHomeCards } from "@/remote/api"
|
||||
|
||||
export const revalidate = 30
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const title = "Шлиссельбургская ДХШ"
|
||||
const m = new MetaBuilder()
|
||||
.setTitle(title)
|
||||
.setData({ url: "/" })
|
||||
.build()
|
||||
return buildMetadata(m)
|
||||
}
|
||||
|
||||
export default async function HomePage() {
|
||||
const items = await getHomeCards()
|
||||
|
||||
return (
|
||||
<App showAuthor>
|
||||
<HomeContent items={items} />
|
||||
</App>
|
||||
)
|
||||
}
|
||||
Vendored
-1
@@ -1,6 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
|
||||
Generated
-10
@@ -16,7 +16,6 @@
|
||||
"date-fns": "^4.0.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"next": "^16.1.0",
|
||||
"next-seo": "^7.1.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-icons": "^5.5.0",
|
||||
@@ -6166,15 +6165,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-seo": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/next-seo/-/next-seo-7.1.0.tgz",
|
||||
"integrity": "sha512-gTWm9wnAXvtqD8QPIpbBA6iMnqMp1WyNTGza9QJPabVIPoX+ZDt1/J0KUkziiIKRyad+nSGLC0BfHgekqcfq0Q==",
|
||||
"peerDependencies": {
|
||||
"next": ">=13.4.0",
|
||||
"react": ">=18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"date-fns": "^4.0.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"next": "^16.1.0",
|
||||
"next-seo": "^7.1.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-icons": "^5.5.0",
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { NextPage } from "next"
|
||||
import { Wrapper } from "@/components/Wrapper"
|
||||
import { NotFound } from "@/components/NotFound"
|
||||
import { Copyright } from "@/components/Copyright"
|
||||
import { yearRange } from "@/const"
|
||||
import { Spacer } from "@/components/Spacer"
|
||||
import { Box } from "@/ui/Box"
|
||||
|
||||
const Index: NextPage = () => (
|
||||
<Wrapper
|
||||
header={null}
|
||||
footer={(
|
||||
<Box as={"footer"}>
|
||||
<Spacer />
|
||||
<Copyright years={yearRange} />
|
||||
<Spacer />
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
<Box vertical>
|
||||
<NotFound />
|
||||
<Spacer />
|
||||
</Box>
|
||||
</Wrapper>
|
||||
)
|
||||
|
||||
export default Index
|
||||
@@ -1,210 +0,0 @@
|
||||
import Head from "next/head"
|
||||
import { App } from "@/components/App"
|
||||
import { Page } from "@/components/Page"
|
||||
import { generateNextSeo } from "next-seo/pages"
|
||||
import { MetaBuilder } from "@/lib/meta"
|
||||
import type { GetStaticProps, NextPage } from "next"
|
||||
import type { BreadcrumbPart, MenuItem, Meta, Tag, Sign, Token } from "@/types"
|
||||
import { Html } from "@/components/Html"
|
||||
import { Youtube } from "@/components/Youtube"
|
||||
import { paramsToSlug } from "@/remote/lib"
|
||||
import { PageGrid } from "@/components/PageGrid"
|
||||
import { useRouter } from "next/router"
|
||||
import { FileCard } from "@/components/FileCard"
|
||||
import { Picture } from "@/ui/Picture"
|
||||
import { tail } from "@/lib/array"
|
||||
import { getMenu, getPageBySlug, getUrls } from "@/remote/api"
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
tags: Tag[]
|
||||
date: string
|
||||
breadcrumb: BreadcrumbPart[]
|
||||
meta?: Meta
|
||||
menu: MenuItem[],
|
||||
tokens: Token[]
|
||||
documentSignature: Sign
|
||||
}
|
||||
|
||||
const Index: NextPage<Props> = props => {
|
||||
const router = useRouter()
|
||||
if (router.isFallback) {
|
||||
return (
|
||||
<div>
|
||||
loading...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
{!props.meta ? null : 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,
|
||||
},
|
||||
})}
|
||||
<title>{props.title}</title>
|
||||
</Head>
|
||||
|
||||
<App
|
||||
contentStyle={{
|
||||
marginTop: "var(--size-l)",
|
||||
marginBottom: "var(--size-xl)",
|
||||
}}
|
||||
menu={props.menu}
|
||||
breadcrumbs={props.breadcrumb}
|
||||
>
|
||||
|
||||
<Page
|
||||
tags={props.tags}
|
||||
date={props.date ? new Date(props.date) : undefined}
|
||||
>
|
||||
<article className={"article"}>
|
||||
{props.tokens.map((x, i) => {
|
||||
switch (x.token) {
|
||||
case "html":
|
||||
return (
|
||||
<Html
|
||||
key={i}
|
||||
html={x.data}
|
||||
/>
|
||||
)
|
||||
|
||||
case "instagram":
|
||||
return (
|
||||
<Html
|
||||
key={i}
|
||||
html={x.data.embed}
|
||||
/>
|
||||
)
|
||||
|
||||
case "youtube":
|
||||
return (
|
||||
<Youtube
|
||||
key={i}
|
||||
url={x.data.url}
|
||||
/>
|
||||
)
|
||||
|
||||
case "image":
|
||||
return (
|
||||
<Picture
|
||||
key={i}
|
||||
src={x.data.src}
|
||||
alt={x.data.alt}
|
||||
width={x.data.width}
|
||||
height={x.data.height}
|
||||
caption={x.data.caption}
|
||||
blur={x.data.blur}
|
||||
wide={x.wide}
|
||||
/>
|
||||
)
|
||||
|
||||
case "file":
|
||||
return (
|
||||
<FileCard
|
||||
key={i}
|
||||
sign={props.documentSignature}
|
||||
{...x.data}
|
||||
/>
|
||||
)
|
||||
|
||||
case "grid":
|
||||
return (
|
||||
<PageGrid
|
||||
key={i}
|
||||
items={x.data.items}
|
||||
/>
|
||||
)
|
||||
|
||||
default:
|
||||
return (
|
||||
<pre key={i}>
|
||||
{JSON.stringify(x)}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</article>
|
||||
</Page>
|
||||
</App>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<Props> = async ctx => {
|
||||
const slug = paramsToSlug(ctx.params?.slug ?? [])
|
||||
const page = await getPageBySlug(slug)
|
||||
if (!page) {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
|
||||
const menu = await getMenu()
|
||||
|
||||
const description = page.description ?? undefined
|
||||
const breadcrumbSize = page?.breadcrumb?.length ?? 0
|
||||
const breadcrumb = breadcrumbSize < 2 ? [] : page.breadcrumb ?? []
|
||||
const meta = (new MetaBuilder())
|
||||
.setImage(page.cover)
|
||||
.setTitle(page.title)
|
||||
.setDescription(description)
|
||||
.build()
|
||||
const tokens = page.tokens
|
||||
|
||||
return {
|
||||
revalidate: 30,
|
||||
props: {
|
||||
tokens,
|
||||
title: page.title,
|
||||
tags: page.tags,
|
||||
date: page.date,
|
||||
meta,
|
||||
menu,
|
||||
breadcrumb,
|
||||
documentSignature: {
|
||||
date: "20.01.2021г.",
|
||||
person: "Тимашева Марина Геннадьевна",
|
||||
position: "Директор",
|
||||
signature: "0ac4ea89753a4ba9893799442325fb41",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
const urls = await getUrls()
|
||||
|
||||
return {
|
||||
fallback: true,
|
||||
paths: urls
|
||||
.map(path => {
|
||||
const slug = tail(path.split("/"))
|
||||
|
||||
return {
|
||||
params: {
|
||||
slug,
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export default Index
|
||||
@@ -1,55 +0,0 @@
|
||||
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 { AppProps } from "next/app"
|
||||
import Head from "next/head"
|
||||
import Script from "next/script"
|
||||
import { YMetrika } from "@/components/YMetrika"
|
||||
import { ThemeColor } from "@/ui/ThemeColor"
|
||||
|
||||
export default function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, maximum-scale=1.0" />
|
||||
<meta name="referrer" content="unsafe-url" />
|
||||
|
||||
<meta name="google-site-verification" content="8BxOTw-Q02DS2KAdcGcnIMwiPZ58DVB18XpOfRibCcg" />
|
||||
<meta name="google-site-verification" content="A-TEemUw4hHpULUDCd6xhxfHnyRbgKtss90JGKXnSpw" />
|
||||
<meta name='yandex-verification' content='643c7dac144b9dac' />
|
||||
<meta name="msvalidate.01" content="E878301FE3E3F12D233A6B1156889601" />
|
||||
<meta name="proculture-verification" content="79110940fd847c3cfe52392ed68f3210" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<link rel="icon" type="image/png" href="/static/graphics/favicon-32.png" />
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/static/graphics/favicon-57.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/static/graphics/favicon-72.png" />
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/static/graphics/favicon-114.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/static/graphics/favicon-144.png" />
|
||||
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/feed+json"
|
||||
title="Шлиссельбургская Детская Художественная Школа"
|
||||
href="/feed.json"
|
||||
/>
|
||||
|
||||
{process.env.NEXT_PUBLIC_YMETRIKA_ACCOUNT && (
|
||||
<YMetrika account={Number(process.env.NEXT_PUBLIC_YMETRIKA_ACCOUNT)} />
|
||||
)}
|
||||
|
||||
<ThemeColor
|
||||
color="white"
|
||||
darkColor="hsl(240, 10%, 10%)"
|
||||
/>
|
||||
</Head>
|
||||
<Script src="https://culturaltracking.ru/static/js/spxl.js?pixelId=27154" data-pixel-id="27154"></Script>
|
||||
|
||||
<Component {...pageProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Html, Head, Main, NextScript } from "next/document"
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
{/* Google Fonts */}
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
@@ -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> = props => {
|
||||
const mobile = useMobile()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
{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,
|
||||
},
|
||||
})}
|
||||
<title>{props.title}</title>
|
||||
</Head>
|
||||
<App
|
||||
showAuthor={true}
|
||||
menu={props.menu}
|
||||
>
|
||||
{mobile ? null : (
|
||||
<HudozkaTitle
|
||||
style={{
|
||||
marginTop: "var(--size-m)",
|
||||
marginBottom: "var(--size-m)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PageGrid
|
||||
items={props.items}
|
||||
/>
|
||||
</App>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<Props> = 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
|
||||
@@ -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 (
|
||||
<MenuContext.Provider value={menu}>
|
||||
<Wrapper
|
||||
header={(
|
||||
<header>
|
||||
{!accessibility ? null : (
|
||||
<AccessibilityPanel/>
|
||||
)}
|
||||
<Wrapper
|
||||
header={(
|
||||
<header>
|
||||
{!accessibility ? null : (
|
||||
<AccessibilityPanel/>
|
||||
)}
|
||||
|
||||
<div className={s.navigation}>
|
||||
<Navigation />
|
||||
</div>
|
||||
{hideBreadcrumbs || !props.breadcrumbs ? null : (
|
||||
<Box center>
|
||||
<Content>
|
||||
<Breadcrumbs
|
||||
items={props.breadcrumbs}
|
||||
path={router.asPath}
|
||||
/>
|
||||
</Content>
|
||||
</Box>
|
||||
)}
|
||||
</header>
|
||||
)}
|
||||
footer={(
|
||||
<Footer>
|
||||
<Box wrap gap={40} align={false} className={s.footerLinks}>
|
||||
<Panel ghost>
|
||||
<Contacts />
|
||||
</Panel>
|
||||
|
||||
<Panel ghost className={s.footerPanel}>
|
||||
<Title level={3}>
|
||||
Версия для слабовидящих
|
||||
</Title>
|
||||
<AccessibilityButton />
|
||||
</Panel>
|
||||
|
||||
<Panel ghost className={s.footerPanel}>
|
||||
<Title level={3}>
|
||||
Анкеты
|
||||
</Title>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<Link href={"https://bus.gov.ru/qrcode/rate/351942"}>
|
||||
Независимая оценка качества оказания услуг
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link href={"https://forms.mkrf.ru/e/2579/xTPLeBU7/?ap_orgcode=600340071"}>
|
||||
Анкета Министерства культуры РФ
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link href={"https://forms.yandex.ru/u/68ef8e9c90fa7b0bacf46e9d/"}>
|
||||
Анкета получателей образовательных услуг ДОП Кировского муниципального района Ленинградской области 2025
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</Panel>
|
||||
<div className={s.navigation}>
|
||||
<Navigation />
|
||||
</div>
|
||||
{hideBreadcrumbs || !props.breadcrumbs ? null : (
|
||||
<Box center>
|
||||
<Content>
|
||||
<Breadcrumbs
|
||||
items={props.breadcrumbs}
|
||||
path={pathname}
|
||||
/>
|
||||
</Content>
|
||||
</Box>
|
||||
)}
|
||||
</header>
|
||||
)}
|
||||
footer={(
|
||||
<Footer>
|
||||
<Box wrap gap={40} align={false} className={s.footerLinks}>
|
||||
<Panel ghost>
|
||||
<Contacts />
|
||||
</Panel>
|
||||
|
||||
<div className={s.gosuslugi}>
|
||||
<Gosuslugi />
|
||||
</div>
|
||||
<Panel ghost className={s.footerPanel}>
|
||||
<Title level={3}>
|
||||
Версия для слабовидящих
|
||||
</Title>
|
||||
<AccessibilityButton />
|
||||
</Panel>
|
||||
|
||||
<Box>
|
||||
<Copyright years={yearRange} />
|
||||
<Panel ghost className={s.footerPanel}>
|
||||
<Title level={3}>
|
||||
Анкеты
|
||||
</Title>
|
||||
|
||||
{!showAuthor ? null : (
|
||||
<>
|
||||
<Spacer as={"span"} />
|
||||
<Author />
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Footer>
|
||||
)}
|
||||
>
|
||||
<Box center>
|
||||
<Content style={props.contentStyle}>
|
||||
{props.children}
|
||||
</Content>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
</MenuContext.Provider>
|
||||
<ul>
|
||||
<li>
|
||||
<Link href={"https://bus.gov.ru/qrcode/rate/351942"}>
|
||||
Независимая оценка качества оказания услуг
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link href={"https://forms.mkrf.ru/e/2579/xTPLeBU7/?ap_orgcode=600340071"}>
|
||||
Анкета Министерства культуры РФ
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link href={"https://forms.yandex.ru/u/68ef8e9c90fa7b0bacf46e9d/"}>
|
||||
Анкета получателей образовательных услуг ДОП Кировского муниципального района Ленинградской области 2025
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</Panel>
|
||||
</Box>
|
||||
|
||||
<div className={s.gosuslugi}>
|
||||
<Gosuslugi />
|
||||
</div>
|
||||
|
||||
<Box>
|
||||
<Copyright years={yearRange} />
|
||||
|
||||
{!showAuthor ? null : (
|
||||
<>
|
||||
<Spacer as={"span"} />
|
||||
<Author />
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Footer>
|
||||
)}
|
||||
>
|
||||
<Box center>
|
||||
<Content style={props.contentStyle}>
|
||||
{props.children}
|
||||
</Content>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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 : (
|
||||
<HudozkaTitle
|
||||
style={{
|
||||
marginTop: "var(--size-m)",
|
||||
marginBottom: "var(--size-m)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PageGrid items={items} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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<MenuProps> = ({ vertical = false, onItemClick, items }) => {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
return (
|
||||
<NavigationMenu.Root>
|
||||
<NavigationMenu.List className={cx(s.menu, { vertical })}>
|
||||
{
|
||||
items.map(item => {
|
||||
const current = router.asPath === item.href
|
||||
const current = pathname === item.href
|
||||
const href = current
|
||||
? undefined
|
||||
: item.href
|
||||
|
||||
@@ -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<HTMLDetailsElement>(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 (
|
||||
<nav className={className}>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"use client"
|
||||
|
||||
import { Page } from "@/components/Page"
|
||||
import { Html } from "@/components/Html"
|
||||
import { Youtube } from "@/components/Youtube"
|
||||
import { Picture } from "@/ui/Picture"
|
||||
import { FileCard } from "@/components/FileCard"
|
||||
import { PageGrid } from "@/components/PageGrid"
|
||||
import type { Tag, Token, Sign } from "@/types"
|
||||
|
||||
export type PageContentProps = {
|
||||
tags: Tag[]
|
||||
date?: string
|
||||
tokens: Token[]
|
||||
documentSignature: Sign
|
||||
}
|
||||
|
||||
export function PageContent({ tags, date, tokens, documentSignature }: PageContentProps) {
|
||||
return (
|
||||
<Page
|
||||
tags={tags}
|
||||
date={date ? new Date(date) : undefined}
|
||||
>
|
||||
<article className={"article"}>
|
||||
{tokens.map((x, i) => {
|
||||
switch (x.token) {
|
||||
case "html":
|
||||
return (
|
||||
<Html
|
||||
key={i}
|
||||
html={x.data}
|
||||
/>
|
||||
)
|
||||
|
||||
case "instagram":
|
||||
return (
|
||||
<Html
|
||||
key={i}
|
||||
html={x.data.embed}
|
||||
/>
|
||||
)
|
||||
|
||||
case "youtube":
|
||||
return (
|
||||
<Youtube
|
||||
key={i}
|
||||
url={x.data.url}
|
||||
/>
|
||||
)
|
||||
|
||||
case "image":
|
||||
return (
|
||||
<Picture
|
||||
key={i}
|
||||
src={x.data.src}
|
||||
alt={x.data.alt}
|
||||
width={x.data.width}
|
||||
height={x.data.height}
|
||||
caption={x.data.caption}
|
||||
blur={x.data.blur}
|
||||
wide={x.wide}
|
||||
/>
|
||||
)
|
||||
|
||||
case "file":
|
||||
return (
|
||||
<FileCard
|
||||
key={i}
|
||||
sign={documentSignature}
|
||||
{...x.data}
|
||||
/>
|
||||
)
|
||||
|
||||
case "grid":
|
||||
return (
|
||||
<PageGrid
|
||||
key={i}
|
||||
items={x.data.items}
|
||||
/>
|
||||
)
|
||||
|
||||
default:
|
||||
return (
|
||||
<pre key={i}>
|
||||
{JSON.stringify(x)}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</article>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
"use client"
|
||||
|
||||
import { MenuContext } from "@/context/MenuContext"
|
||||
import type { MenuItem } from "@/types"
|
||||
|
||||
export type ProvidersProps = {
|
||||
menu: MenuItem[]
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function Providers({ menu, children }: ProvidersProps) {
|
||||
return (
|
||||
<MenuContext.Provider value={menu}>
|
||||
{children}
|
||||
</MenuContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import s from "./wrapper.module.css"
|
||||
|
||||
import cx from "classnames"
|
||||
|
||||
+66
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { meta, MetaBuilder } from "./meta"
|
||||
import { meta, MetaBuilder, buildMetadata } from "./meta"
|
||||
|
||||
describe("meta", () => {
|
||||
it("should return defaults when called with empty object", () => {
|
||||
@@ -59,3 +59,68 @@ describe("MetaBuilder", () => {
|
||||
expect(result.type).toBe("article")
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildMetadata", () => {
|
||||
const defaultMeta = meta({})
|
||||
|
||||
it("should map title and description", () => {
|
||||
const result = buildMetadata(defaultMeta)
|
||||
expect(result.title).toBe("Шлиссельбургская ДХШ")
|
||||
expect(result.description).toBe("Сайт Шлиссельбургской художественной школы")
|
||||
})
|
||||
|
||||
it("should map openGraph fields", () => {
|
||||
const result = buildMetadata(defaultMeta)
|
||||
const og = result.openGraph
|
||||
expect(og).toBeDefined()
|
||||
expect(og).toMatchObject({
|
||||
url: "https://art.shlisselburg.org/",
|
||||
title: "Шлиссельбургская ДХШ",
|
||||
description: "Сайт Шлиссельбургской художественной школы",
|
||||
siteName: "Шлиссельбургская Детская Художественная Школа",
|
||||
locale: "ru_RU",
|
||||
type: "website",
|
||||
})
|
||||
})
|
||||
|
||||
it("should map openGraph images", () => {
|
||||
const result = buildMetadata(defaultMeta)
|
||||
const images = (result.openGraph as { images: unknown[] }).images
|
||||
expect(images).toHaveLength(1)
|
||||
expect(images[0]).toEqual({
|
||||
url: "https://art.shlisselburg.org/entrance.jpg",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
})
|
||||
})
|
||||
|
||||
it("should map twitter fields", () => {
|
||||
const result = buildMetadata(defaultMeta)
|
||||
expect(result.twitter).toEqual({
|
||||
card: "summary_large_image",
|
||||
site: "@",
|
||||
creator: "@tmshv",
|
||||
})
|
||||
})
|
||||
|
||||
it("should use custom values from MetaBuilder", () => {
|
||||
const m = new MetaBuilder()
|
||||
.setTitle("Custom Page")
|
||||
.setDescription("A custom description")
|
||||
.setImage({ src: "https://example.com/cover.jpg", width: 800, height: 400 })
|
||||
.setData({ url: "/custom" })
|
||||
.build()
|
||||
|
||||
const result = buildMetadata(m)
|
||||
expect(result.title).toBe("Custom Page")
|
||||
expect(result.description).toBe("A custom description")
|
||||
|
||||
const og = result.openGraph as { url: string, images: { url: string, width: number, height: number }[] }
|
||||
expect(og.url).toBe("https://art.shlisselburg.org/custom")
|
||||
expect(og.images[0]).toEqual({
|
||||
url: "https://example.com/cover.jpg",
|
||||
width: 800,
|
||||
height: 400,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Metadata } from "next"
|
||||
import type { Meta, Pic } from "@/types"
|
||||
|
||||
export function meta(props: Partial<Meta>): Meta {
|
||||
@@ -80,3 +81,24 @@ export class MetaBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildMetadata(m: Meta): Metadata {
|
||||
return {
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
openGraph: {
|
||||
url: m.url,
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
siteName: m.siteName,
|
||||
locale: m.locale,
|
||||
type: m.type as "website",
|
||||
images: [{ url: m.image, width: m.imageWidth, height: m.imageHeight }],
|
||||
},
|
||||
twitter: {
|
||||
card: m.twitterCard as "summary_large_image",
|
||||
site: m.twitterSite,
|
||||
creator: m.twitterCreator,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user