From 089436442e559b83792f3a6cebeae566dc0dad42 Mon Sep 17 00:00:00 2001 From: Roman Timashev Date: Tue, 10 Feb 2026 18:53:15 +0700 Subject: [PATCH 01/17] dev compose updated --- compose-dev.yml | 36 +++++++++++++++++++----------------- frontend/Dockerfile | 3 +++ 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/compose-dev.yml b/compose-dev.yml index 6197771..59be97a 100644 --- a/compose-dev.yml +++ b/compose-dev.yml @@ -1,28 +1,30 @@ services: - payload: + app: build: - context: payload - dockerfile: Dockerfile + context: frontend + args: + BACKEND_URL: http://backend:8080 + NEXT_PUBLIC_MAPLIBRE_STYLE: ${NEXT_PUBLIC_MAPLIBRE_STYLE} + NEXT_PUBLIC_MAPBOX_STYLE: ${NEXT_PUBLIC_MAPBOX_STYLE:-mapbox://styles/mapbox/standard} + NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN: ${NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN} restart: unless-stopped environment: - DATABASE_URI: mongodb://root:password0@mongo:27017/payload-db?authSource=admin - PAYLOAD_SECRET: supersecret0 + BACKEND_URL: http://backend:8080 ports: - - "5000:3000" + - "3000:3000" depends_on: - - mongo - links: - - mongo + - backend + labels: + dev.orbstack.domains: sosbor.local - mongo: - image: mongo:8-noble + backend: + build: + context: pb + restart: unless-stopped ports: - - "27017:27017" + - "8080:8080" volumes: - - data:/data/db - environment: - MONGO_INITDB_ROOT_USERNAME: root - MONGO_INITDB_ROOT_PASSWORD: password0 + - pb_data:/pb/pb_data volumes: - data: + pb_data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index b08d81f..388f039 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -31,6 +31,9 @@ ENV NEXT_PUBLIC_MAPBOX_STYLE=$NEXT_PUBLIC_MAPBOX_STYLE ARG NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=X ENV NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=$NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN +ARG BACKEND_URL=http://127.0.0.1:8080 +ENV BACKEND_URL=$BACKEND_URL + RUN npm run build From 2a5c9f9ada1253f613833afe3fda597534441dd7 Mon Sep 17 00:00:00 2001 From: Roman Timashev Date: Tue, 10 Feb 2026 18:53:26 +0700 Subject: [PATCH 02/17] update pb version to latest --- pb/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pb/Dockerfile b/pb/Dockerfile index e645c4a..5c8bcab 100644 --- a/pb/Dockerfile +++ b/pb/Dockerfile @@ -1,6 +1,6 @@ FROM alpine:latest -ARG PB_VERSION=0.35.0 +ARG PB_VERSION=0.36.2 RUN apk add --no-cache \ unzip \ From 58a8b7450674c1ed1b5976c146fbd15a0b8e013e Mon Sep 17 00:00:00 2001 From: Roman Timashev Date: Tue, 10 Feb 2026 23:35:17 +0700 Subject: [PATCH 03/17] first try --- frontend/src/components/AddButton/index.tsx | 2 + frontend/src/components/Header/index.tsx | 119 +++++ frontend/src/components/IdeaModal/index.tsx | 10 +- frontend/src/components/IndexPage/index.tsx | 21 +- frontend/src/components/Map/index.tsx | 8 +- frontend/src/components/MapMapbox/index.tsx | 2 + .../src/components/SubmissionFeed/Item.tsx | 9 +- .../src/components/SubmissionFeed/index.tsx | 17 +- frontend/src/components/SurveyModal/index.tsx | 2 + frontend/src/contexts/clientId.tsx | 2 + frontend/src/contexts/form.tsx | 2 + frontend/src/contexts/hasMounted.ts | 2 + frontend/src/contexts/navbar.tsx | 2 + frontend/src/hooks/useOpenSurveyModal.ts | 23 + frontend/src/lib/navigation.ts | 22 + frontend/src/pages/_app.tsx | 408 +----------------- frontend/src/pages/map.tsx | 18 +- frontend/src/theme.ts | 111 +++++ 18 files changed, 351 insertions(+), 429 deletions(-) create mode 100644 frontend/src/components/Header/index.tsx create mode 100644 frontend/src/hooks/useOpenSurveyModal.ts create mode 100644 frontend/src/lib/navigation.ts create mode 100644 frontend/src/theme.ts diff --git a/frontend/src/components/AddButton/index.tsx b/frontend/src/components/AddButton/index.tsx index 1aa3874..a12fa18 100644 --- a/frontend/src/components/AddButton/index.tsx +++ b/frontend/src/components/AddButton/index.tsx @@ -1,3 +1,5 @@ +'use client' + import { FormContext } from '@/contexts/form'; import { Popover, Button, Center, Box } from '@mantine/core'; import { useModals } from '@mantine/modals'; diff --git a/frontend/src/components/Header/index.tsx b/frontend/src/components/Header/index.tsx new file mode 100644 index 0000000..7027aa6 --- /dev/null +++ b/frontend/src/components/Header/index.tsx @@ -0,0 +1,119 @@ +'use client' + +import { AppShell, Burger, Button, Center, Flex, Group, Text } from '@mantine/core' +import Link from 'next/link' +import { useMediaQuery } from '@mantine/hooks' +import { navButtons } from '@/lib/navigation' + +export type HeaderProps = { + height: number + position?: 'sticky' | undefined + mobileOpened: boolean + toggleMobile: () => void + onSurveyClick: () => void + navButtonColor?: string +} + +export function Header({ height, position, mobileOpened, toggleMobile, onSurveyClick, navButtonColor = 'white' }: HeaderProps) { + const isMobile = useMediaQuery('(max-width: 768px)', true) + const isTablet = useMediaQuery('(max-width: 1024px)', true) + + return ( + +
+ + + + СОСНОВЫЙ БОР + + + + + {navButtons.map(x => x.href ? ( + + ) : ( + + ))} + + +
+
+ ) +} diff --git a/frontend/src/components/IdeaModal/index.tsx b/frontend/src/components/IdeaModal/index.tsx index 7beb6b1..0beec0c 100644 --- a/frontend/src/components/IdeaModal/index.tsx +++ b/frontend/src/components/IdeaModal/index.tsx @@ -1,8 +1,10 @@ +'use client' + import { FormContext } from '@/contexts/form' import { Text, Stack, Button, Title, Center, Textarea, Tooltip } from '@mantine/core' import { useModals } from '@mantine/modals' import type { ContextModalProps } from '@mantine/modals' -import { useRouter } from 'next/router' +import { usePathname } from 'next/navigation' import { useContext, useEffect, useState } from 'react' import { useForm, Controller } from 'react-hook-form' import { z } from 'zod' @@ -130,12 +132,12 @@ export function IdeaModal({ id: modalId, innerProps }: ContextModalProps { - if (router.pathname == '/') { + if (pathname == '/') { modals.closeAll() } - }, [router.pathname, modals]) + }, [pathname, modals]) return (
{ - modals.openContextModal( - 'survey', - { - centered: true, - size: 'min(100%, 900px)', - withCloseButton: false, - closeOnEscape: false, - closeOnClickOutside: false, - innerProps: { - defaultValues: {}, - }, - } - ) - } + const openSurveyModal = useOpenSurveyModal() return ( <> diff --git a/frontend/src/components/Map/index.tsx b/frontend/src/components/Map/index.tsx index 605c3e8..fef9265 100644 --- a/frontend/src/components/Map/index.tsx +++ b/frontend/src/components/Map/index.tsx @@ -1,7 +1,9 @@ +'use client' + import { useContext } from 'react' import { Layer, Marker, Source } from 'react-map-gl/mapbox' import { useModals } from '@mantine/modals' -import { useRouter } from 'next/router' +import { useSearchParams } from 'next/navigation' import { FormContext } from '@/contexts/form' import useSWR from 'swr' import { Popover, ScrollArea, Text } from '@mantine/core' @@ -62,8 +64,8 @@ export function Map({ initialCoords }: MapProps) { } const isMobile = useMediaQuery('(max-width: 768px)', true, { getInitialValueInEffect: false }) - const router = useRouter() - const isPreview = Boolean(router.query?.preview) == true + const searchParams = useSearchParams() + const isPreview = Boolean(searchParams.get('preview')) const features: Submission[] = (data?.items ?? []) .filter((x: Submission) => x?.feature && JSON.stringify(x?.feature) !== '{}') diff --git a/frontend/src/components/MapMapbox/index.tsx b/frontend/src/components/MapMapbox/index.tsx index f38adee..6cff777 100644 --- a/frontend/src/components/MapMapbox/index.tsx +++ b/frontend/src/components/MapMapbox/index.tsx @@ -1,3 +1,5 @@ +'use client' + import React from 'react' import { Map, GeolocateControl, NavigationControl } from 'react-map-gl/mapbox' import type { MapProps } from 'react-map-gl/mapbox' diff --git a/frontend/src/components/SubmissionFeed/Item.tsx b/frontend/src/components/SubmissionFeed/Item.tsx index c5f7269..9fda0b7 100644 --- a/frontend/src/components/SubmissionFeed/Item.tsx +++ b/frontend/src/components/SubmissionFeed/Item.tsx @@ -1,10 +1,12 @@ +'use client' + import { Card, Stack, Text, Group, ScrollArea, ActionIcon } from '@mantine/core' import { useDisclosure } from '@mantine/hooks' import type { Submission } from '.' import { useMap } from 'react-map-gl/maplibre' import { NavbarContext } from '@/contexts/navbar' import { useContext } from 'react' -import { useRouter } from 'next/router' +import { useSearchParams } from 'next/navigation' import { useEffectOnce } from 'react-use' type ItemProps = { @@ -12,13 +14,14 @@ type ItemProps = { } export function Item({ data }: ItemProps) { - const router = useRouter() + const searchParams = useSearchParams() + const pointId = searchParams.get('pointId') const [, { toggle }] = useDisclosure(false) const { setDrawer } = useContext(NavbarContext) const { default: map } = useMap() useEffectOnce(() => { - if (router.query?.pointId == data.id) { + if (pointId == data.id) { toggle() } }) diff --git a/frontend/src/components/SubmissionFeed/index.tsx b/frontend/src/components/SubmissionFeed/index.tsx index dd58680..0dc8394 100644 --- a/frontend/src/components/SubmissionFeed/index.tsx +++ b/frontend/src/components/SubmissionFeed/index.tsx @@ -1,3 +1,5 @@ +'use client' + import { Stack, ScrollArea, Skeleton, Box, Button, ActionIcon, Group, Text, Alert } from '@mantine/core' import { useHasMounted } from '@/contexts/hasMounted' import { Item } from './Item' @@ -5,7 +7,7 @@ import useSWRInfinite from 'swr/infinite' import { NavbarContext } from '@/contexts/navbar' import { useContext } from 'react' import s from './index.module.css' -import { useRouter } from 'next/router' +import { useSearchParams } from 'next/navigation' import { useEffectOnce } from 'react-use' import { AddButton } from '../AddButton' import { useMediaQuery } from '@mantine/hooks' @@ -14,7 +16,8 @@ import type { Feature, Submission, SubmissionResponse } from '@/types/submission export type { Feature, Submission, SubmissionResponse } export function SubmissionFeed() { - const router = useRouter() + const searchParams = useSearchParams() + const pointId = searchParams.get('pointId') const hasMounted = useHasMounted() const isMobile = useMediaQuery('(max-width: 768px)', true) const { data, error, isLoading, size, setSize } = useSWRInfinite( @@ -34,16 +37,16 @@ export function SubmissionFeed() { : data .flatMap(x => x.items) .sort((a, b) => { - if (!router.query?.pointId) return 0 - if (a.id == router.query?.pointId) return -1 - if (b.id == router.query?.pointId) return 1 + if (!pointId) return 0 + if (a.id == pointId) return -1 + if (b.id == pointId) return 1 return 0 }) // on query.pointId fetch up to amount of items in /index useEffectOnce(() => { - if (router.query?.pointId) { - if (!dataFlat.find(x => x.id == router.query?.pointId)) { + if (pointId) { + if (!dataFlat.find(x => x.id == pointId)) { setSize(10) } } diff --git a/frontend/src/components/SurveyModal/index.tsx b/frontend/src/components/SurveyModal/index.tsx index 6f6d57e..84c840a 100644 --- a/frontend/src/components/SurveyModal/index.tsx +++ b/frontend/src/components/SurveyModal/index.tsx @@ -1,3 +1,5 @@ +'use client' + import { Text, Select, Button, Textarea, Tabs, Group, Fieldset, Title, Stack, Center, Space, CloseButton, Slider, Box } from '@mantine/core' import { useModals } from '@mantine/modals' import { useCallback, useRef, useState } from 'react' diff --git a/frontend/src/contexts/clientId.tsx b/frontend/src/contexts/clientId.tsx index 04da7d5..6750baa 100644 --- a/frontend/src/contexts/clientId.tsx +++ b/frontend/src/contexts/clientId.tsx @@ -1,3 +1,5 @@ +'use client' + import { createContext, useContext, useEffect } from 'react' import type { Context } from 'react' import { useCookie, useLocalStorage } from 'react-use' diff --git a/frontend/src/contexts/form.tsx b/frontend/src/contexts/form.tsx index c8e6bdf..5aaf2ec 100644 --- a/frontend/src/contexts/form.tsx +++ b/frontend/src/contexts/form.tsx @@ -1,3 +1,5 @@ +'use client' + import { createContext, useState } from 'react' import type { Context } from 'react' import type { FormContextValue, FormData } from '@/types' diff --git a/frontend/src/contexts/hasMounted.ts b/frontend/src/contexts/hasMounted.ts index f8714a1..cd8c58a 100644 --- a/frontend/src/contexts/hasMounted.ts +++ b/frontend/src/contexts/hasMounted.ts @@ -1,3 +1,5 @@ +'use client' + import { useSyncExternalStore } from 'react'; const emptySubscribe = () => () => {}; diff --git a/frontend/src/contexts/navbar.tsx b/frontend/src/contexts/navbar.tsx index a900c16..40744de 100644 --- a/frontend/src/contexts/navbar.tsx +++ b/frontend/src/contexts/navbar.tsx @@ -1,3 +1,5 @@ +'use client' + import { createContext, useState } from 'react' import type { Context } from 'react' import type { NavbarContextValue } from '@/types' diff --git a/frontend/src/hooks/useOpenSurveyModal.ts b/frontend/src/hooks/useOpenSurveyModal.ts new file mode 100644 index 0000000..3b9d0ab --- /dev/null +++ b/frontend/src/hooks/useOpenSurveyModal.ts @@ -0,0 +1,23 @@ +'use client' + +import { useModals } from '@mantine/modals' + +export function useOpenSurveyModal() { + const modals = useModals() + + return () => { + modals.openContextModal( + 'survey', + { + centered: true, + size: 'min(100%, 900px)', + withCloseButton: false, + closeOnEscape: false, + closeOnClickOutside: false, + innerProps: { + defaultValues: {}, + }, + } + ) + } +} diff --git a/frontend/src/lib/navigation.ts b/frontend/src/lib/navigation.ts new file mode 100644 index 0000000..dfe53ef --- /dev/null +++ b/frontend/src/lib/navigation.ts @@ -0,0 +1,22 @@ +import type { Button } from '@mantine/core' + +export type NavButton = { + text: string + href: string | null + props?: React.ComponentPropsWithoutRef +} + +export const navButtons: NavButton[] = [ + { + text: 'График проекта', + href: '/#timeline', + }, + { + text: 'Карта идей', + href: '/map', + }, + { + text: 'Пройти опрос', + href: null, + }, +] diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index ac6742a..97169cc 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -1,9 +1,9 @@ import type { AppProps } from 'next/app' -import { AppShell, Box, Burger, Button, Center, createTheme, Drawer, Fieldset, Flex, Group, MantineProvider, Stack, Text, Title } from '@mantine/core' +import { AppShell, Box, Button, Center, Drawer, Flex, Group, MantineProvider, Stack, Text } from '@mantine/core' import Link from 'next/link' import { NextSeo } from 'next-seo' import { useDisclosure, useMediaQuery } from '@mantine/hooks' -import { ModalsProvider, useModals } from '@mantine/modals' +import { ModalsProvider } from '@mantine/modals' import { IdeaModal } from '@/components/IdeaModal' import { SurveyModal } from '@/components/SurveyModal' import { MapProvider } from 'react-map-gl/maplibre' @@ -12,158 +12,21 @@ import { SWRConfig } from 'swr' import { NavbarContext, NavbarContextProvider } from '@/contexts/navbar' import { useContext } from 'react' import { SubmissionFeed } from '@/components/SubmissionFeed' -import { Golos_Text } from 'next/font/google' -const fontVar = Golos_Text({ weight: ['400', '700'], subsets: ['latin', 'cyrillic'] }) -import mobileMenu from '../styles/mobileMenu.module.css' -import groupStyles from '../styles/group.module.css' -import textStyles from '../styles/text.module.css' -import titleStyles from '../styles/title.module.css' -import buttonStyles from '../styles/button.module.css' -import appShellStyles from '../styles/appShell.module.css' import '@mantine/core/styles.css' import { FormContextProvider } from '@/contexts/form' -import type { MantineColorArray } from '@/types' -const createColorTuple = (color: string): MantineColorArray => - [color, color, color, color, color, color, color, color, color, color] - -const theme = createTheme({ - colors: { - primary: createColorTuple('rgb(233 79 43)'), - secondary: createColorTuple('rgb(155 185 98)'), - third: createColorTuple('rgb(247 236 209)'), - dark: createColorTuple('rgb(4,30,73)'), - black: createColorTuple('#1E1928'), - }, - defaultRadius: 0, - headings: { - fontWeight: '600', - sizes: { - h1: { - fontSize: '60px', - lineHeight: '80px', - }, - h2: { - fontSize: '48px', - lineHeight: '48px', - }, - }, - }, - components: { - AppShell: AppShell.extend({ - styles: { - root: fontVar.style, - }, - }), - Title: Title.extend({ - defaultProps: { - c: 'third', - }, - styles: { - root: { - ...fontVar.style, - fontWeight: '400', - }, - }, - classNames: titleStyles - }), - Text: Text.extend({ - classNames: textStyles, - defaultProps: { - size: '20px', - }, - styles: { - root: { - color: '#1E1928CC', - lineHeight: '28px', - }, - } - }), - Button: Button.extend({ - defaultProps: { - c: 'white', - }, - styles: { - root: { - minHeight: 64, - outline: 'solid 1px var(--mantine-color-secondary-1)', - outlineOffset: 2, - }, - label: { - fontSize: '20px', - lineHeight: '20px', - padding: '0px 30px', - fontWeight: 700, - }, - }, - classNames: buttonStyles - }), - Group: Group.extend({ - classNames: groupStyles, - }), - Fieldset: Fieldset.extend({ - styles: { - legend: { - fontWeight: 'bold', - } - } - }) - }, - fontSizes: { - lg: '20px', - xs: '14px', - base: '14px', - }, - lineHeights: { - lg: '29px', - xs: '18px', - base: '18px', - }, -}) - -type NavButton = { - text: string - href: string | null - props?: React.ComponentPropsWithoutRef -} - -const navButtons: NavButton[] = [ - { - text: 'График проекта', - href: '/#timeline', - }, - { - text: 'Карта идей', - href: '/map', - }, - { - text: 'Пройти опрос', - href: null, - }, -] +import { theme, fontVar, mobileMenu, appShellStyles } from '@/theme' +import { navButtons } from '@/lib/navigation' +import { useOpenSurveyModal } from '@/hooks/useOpenSurveyModal' +import { Header } from '@/components/Header' const MapPageLayout = ({ children }: { children: React.ReactNode }) => { const { drawer, setDrawer } = useContext(NavbarContext) const isMobile = useMediaQuery('(max-width: 768px)', true) - const isTablet = useMediaQuery('(max-width: 1024px)', true) const [mobileOpened, { toggle: toggleMobile }] = useDisclosure() - const modals = useModals() - const openSurveyModal = () => { - modals.openContextModal( - 'survey', - { - centered: true, - size: 'min(100%, 900px)', - withCloseButton: false, - closeOnEscape: false, - closeOnClickOutside: false, - innerProps: { - defaultValues: {}, - }, - } - ) - } + const openSurveyModal = useOpenSurveyModal() + return ( { aside={{ width: '100%', breakpoint: 'lg', collapsed: { desktop: true, mobile: !mobileOpened } }} navbar={{ width: !drawer ? 400 : 0, breakpoint: 'lg', collapsed: { desktop: false, mobile: !drawer } }} > - -
- - - {/* */} - - СОСНОВЫЙ БОР - - - - - {navButtons.map(x => x.href ? ( - - ) : ( - - ))} - - -
-
+
{ - ) : ( - - ))} - - - - +
{ p='14px 26px' > - {/* */} { sm: 100, }} > - {/* - */} {children}
{
- - {/* - - */} ) } diff --git a/frontend/src/pages/map.tsx b/frontend/src/pages/map.tsx index a1b06e1..59b32e9 100644 --- a/frontend/src/pages/map.tsx +++ b/frontend/src/pages/map.tsx @@ -1,6 +1,6 @@ import React, { useContext, useEffect, useState } from 'react' import { useModals } from '@mantine/modals' -import { useRouter } from 'next/router' +import { useSearchParams } from 'next/navigation' import { SWRConfig } from 'swr' import type { NextPage, GetServerSidePropsContext } from 'next/types' import { Map } from '@/components/Map' @@ -9,11 +9,12 @@ import { NavbarContext } from '@/contexts/navbar' const PageComponent: React.FC = () => { const modals = useModals() - const router = useRouter() + const searchParams = useSearchParams() const { setDrawer } = useContext(NavbarContext) - const previewFeature = Boolean(router.query?.preview) == true - const coords = (router.query?.preview as string | undefined)?.split(',') + const preview = searchParams.get('preview') + const previewFeature = Boolean(preview) + const coords = preview?.split(',') const [initalCoords] = useState(previewFeature && coords ? { @@ -26,7 +27,7 @@ const PageComponent: React.FC = () => { } ) - const autoOpenModal = Boolean(router.query?.idea) == true + const autoOpenModal = Boolean(searchParams.get('idea')) useEffect(() => { if (autoOpenModal) { @@ -35,7 +36,6 @@ const PageComponent: React.FC = () => { { centered: true, size: 'min(100%, 650px)', - // radius: 'xl', withCloseButton: false, innerProps: { @@ -46,10 +46,8 @@ const PageComponent: React.FC = () => { }, [autoOpenModal, modals]) useEffect(() => { - if (router.pathname == '/map') { - setDrawer(false) - } - }, [router.pathname, setDrawer]) + setDrawer(false) + }, [setDrawer]) return ( <> diff --git a/frontend/src/theme.ts b/frontend/src/theme.ts new file mode 100644 index 0000000..25a7d86 --- /dev/null +++ b/frontend/src/theme.ts @@ -0,0 +1,111 @@ +import { AppShell, Button, Fieldset, Group, Text, Title, createTheme } from '@mantine/core' +import { Golos_Text } from 'next/font/google' +import type { MantineColorArray } from '@/types' + +import mobileMenu from './styles/mobileMenu.module.css' +import groupStyles from './styles/group.module.css' +import textStyles from './styles/text.module.css' +import titleStyles from './styles/title.module.css' +import buttonStyles from './styles/button.module.css' +import appShellStyles from './styles/appShell.module.css' + +export { mobileMenu, groupStyles, textStyles, titleStyles, buttonStyles, appShellStyles } + +export const fontVar = Golos_Text({ weight: ['400', '700'], subsets: ['latin', 'cyrillic'] }) + +export const createColorTuple = (color: string): MantineColorArray => + [color, color, color, color, color, color, color, color, color, color] + +export const theme = createTheme({ + colors: { + primary: createColorTuple('rgb(233 79 43)'), + secondary: createColorTuple('rgb(155 185 98)'), + third: createColorTuple('rgb(247 236 209)'), + dark: createColorTuple('rgb(4,30,73)'), + black: createColorTuple('#1E1928'), + }, + defaultRadius: 0, + headings: { + fontWeight: '600', + sizes: { + h1: { + fontSize: '60px', + lineHeight: '80px', + }, + h2: { + fontSize: '48px', + lineHeight: '48px', + }, + }, + }, + components: { + AppShell: AppShell.extend({ + styles: { + root: fontVar.style, + }, + }), + Title: Title.extend({ + defaultProps: { + c: 'third', + }, + styles: { + root: { + ...fontVar.style, + fontWeight: '400', + }, + }, + classNames: titleStyles + }), + Text: Text.extend({ + classNames: textStyles, + defaultProps: { + size: '20px', + }, + styles: { + root: { + color: '#1E1928CC', + lineHeight: '28px', + }, + } + }), + Button: Button.extend({ + defaultProps: { + c: 'white', + }, + styles: { + root: { + minHeight: 64, + outline: 'solid 1px var(--mantine-color-secondary-1)', + outlineOffset: 2, + }, + label: { + fontSize: '20px', + lineHeight: '20px', + padding: '0px 30px', + fontWeight: 700, + }, + }, + classNames: buttonStyles + }), + Group: Group.extend({ + classNames: groupStyles, + }), + Fieldset: Fieldset.extend({ + styles: { + legend: { + fontWeight: 'bold', + } + } + }) + }, + fontSizes: { + lg: '20px', + xs: '14px', + base: '14px', + }, + lineHeights: { + lg: '29px', + xs: '18px', + base: '18px', + }, +}) From ebc6bf1f1ed0599abbaacd271637941d30f23890 Mon Sep 17 00:00:00 2001 From: Roman Timashev Date: Wed, 11 Feb 2026 01:24:41 +0700 Subject: [PATCH 04/17] migrate pages -> app --- .../(default)/debug/page.tsx} | 9 +- frontend/src/app/(default)/layout.tsx | 164 +++++++++ frontend/src/app/(default)/page.tsx | 17 + frontend/src/app/(map)/layout.tsx | 123 +++++++ .../(map)/map/MapPageContent.tsx} | 60 +--- frontend/src/app/(map)/map/page.tsx | 10 + .../api/indexFeedback/route.ts} | 12 +- frontend/src/app/layout.tsx | 27 ++ frontend/src/app/providers.tsx | 38 ++ .../src/components/IndexPage/HeroSection.tsx | 116 ++++++ frontend/src/components/IndexPage/MapCTA.tsx | 136 +++++++ .../src/components/IndexPage/Sponsors.tsx | 69 ++++ .../src/components/IndexPage/SurveyCTA.tsx | 113 ++++++ .../components/IndexPage/TimelineSection.tsx | 39 ++ .../SurveyModal/CheckboxWithOther.tsx | 2 + frontend/src/fonts.ts | 3 + frontend/src/pages/_app.tsx | 339 ------------------ frontend/src/pages/_document.tsx | 16 - frontend/src/pages/index.tsx | 35 -- frontend/src/theme.ts | 6 +- frontend/tsconfig.json | 11 +- 21 files changed, 890 insertions(+), 455 deletions(-) rename frontend/src/{pages/debug.tsx => app/(default)/debug/page.tsx} (53%) create mode 100644 frontend/src/app/(default)/layout.tsx create mode 100644 frontend/src/app/(default)/page.tsx create mode 100644 frontend/src/app/(map)/layout.tsx rename frontend/src/{pages/map.tsx => app/(map)/map/MapPageContent.tsx} (53%) create mode 100644 frontend/src/app/(map)/map/page.tsx rename frontend/src/{pages/api/indexFeedback.ts => app/api/indexFeedback/route.ts} (54%) create mode 100644 frontend/src/app/layout.tsx create mode 100644 frontend/src/app/providers.tsx create mode 100644 frontend/src/components/IndexPage/HeroSection.tsx create mode 100644 frontend/src/components/IndexPage/MapCTA.tsx create mode 100644 frontend/src/components/IndexPage/Sponsors.tsx create mode 100644 frontend/src/components/IndexPage/SurveyCTA.tsx create mode 100644 frontend/src/components/IndexPage/TimelineSection.tsx create mode 100644 frontend/src/fonts.ts delete mode 100644 frontend/src/pages/_app.tsx delete mode 100644 frontend/src/pages/_document.tsx delete mode 100644 frontend/src/pages/index.tsx diff --git a/frontend/src/pages/debug.tsx b/frontend/src/app/(default)/debug/page.tsx similarity index 53% rename from frontend/src/pages/debug.tsx rename to frontend/src/app/(default)/debug/page.tsx index ff6c49a..e28b9d2 100644 --- a/frontend/src/pages/debug.tsx +++ b/frontend/src/app/(default)/debug/page.tsx @@ -1,16 +1,11 @@ -function useDebug() { +export default function DebugPage() { const log = [ `NEXT_PUBLIC_MAPLIBRE_STYLE=${process.env.NEXT_PUBLIC_MAPLIBRE_STYLE}` ] - return log.join('\n') -} - -export default function Page() { - const debug = useDebug() return (
-            {debug}
+            {log.join('\n')}
         
) } diff --git a/frontend/src/app/(default)/layout.tsx b/frontend/src/app/(default)/layout.tsx new file mode 100644 index 0000000..c3d77ef --- /dev/null +++ b/frontend/src/app/(default)/layout.tsx @@ -0,0 +1,164 @@ +'use client' + +import { AppShell, Box, Button, Center, Drawer, Flex, Group, Stack, Text } from '@mantine/core' +import Link from 'next/link' +import { useDisclosure, useMediaQuery } from '@mantine/hooks' +import { useOpenSurveyModal } from '@/hooks/useOpenSurveyModal' +import { Header } from '@/components/Header' +import { mobileMenu, appShellStyles } from '@/theme' +import { navButtons } from '@/lib/navigation' + +export default function DefaultLayout({ children }: { children: React.ReactNode }) { + const [mobileOpened, { toggle: toggleMobile }] = useDisclosure() + const isMobile = useMediaQuery('(max-width: 768px)') + const openSurveyModal = useOpenSurveyModal() + + return ( + +
+ + + + + + СОСНОВЫЙ БОР + + + + + + + {navButtons.map(x => x.href ? ( + + ) : ( + + ))} + + + + + +
+ + {children} + +
+ + + Мастер-план Сосновоборского городского округа + + + Copyright © 2026 design unit 4 & creators + + +
+ + + ) +} diff --git a/frontend/src/app/(default)/page.tsx b/frontend/src/app/(default)/page.tsx new file mode 100644 index 0000000..033b43c --- /dev/null +++ b/frontend/src/app/(default)/page.tsx @@ -0,0 +1,17 @@ +import { HeroSection } from '@/components/IndexPage/HeroSection' +import { TimelineSection } from '@/components/IndexPage/TimelineSection' +import { MapCTA } from '@/components/IndexPage/MapCTA' +import { SurveyCTA } from '@/components/IndexPage/SurveyCTA' +import { Sponsors } from '@/components/IndexPage/Sponsors' + +export default function Page() { + return ( + <> + + + + + + + ) +} diff --git a/frontend/src/app/(map)/layout.tsx b/frontend/src/app/(map)/layout.tsx new file mode 100644 index 0000000..f9210e4 --- /dev/null +++ b/frontend/src/app/(map)/layout.tsx @@ -0,0 +1,123 @@ +'use client' + +import { AppShell, Button, Stack } from '@mantine/core' +import Link from 'next/link' +import { Suspense, useContext } from 'react' +import { useDisclosure, useMediaQuery } from '@mantine/hooks' +import { NavbarContext } from '@/contexts/navbar' +import { SubmissionFeed } from '@/components/SubmissionFeed' +import { useOpenSurveyModal } from '@/hooks/useOpenSurveyModal' +import { Header } from '@/components/Header' +import { navButtons } from '@/lib/navigation' + +export default function MapLayout({ children }: { children: React.ReactNode }) { + const { drawer, setDrawer } = useContext(NavbarContext) + const isMobile = useMediaQuery('(max-width: 768px)', true) + const [mobileOpened, { toggle: toggleMobile }] = useDisclosure() + const openSurveyModal = useOpenSurveyModal() + + return ( + +
+ + + + + + + + + + + + {navButtons.map(x => x.href ? ( + + ) : ( + + ))} + + + + + {children} + + + ) +} diff --git a/frontend/src/pages/map.tsx b/frontend/src/app/(map)/map/MapPageContent.tsx similarity index 53% rename from frontend/src/pages/map.tsx rename to frontend/src/app/(map)/map/MapPageContent.tsx index 59b32e9..0588788 100644 --- a/frontend/src/pages/map.tsx +++ b/frontend/src/app/(map)/map/MapPageContent.tsx @@ -1,13 +1,13 @@ -import React, { useContext, useEffect, useState } from 'react' +'use client' + +import { useContext, useEffect, useState } from 'react' import { useModals } from '@mantine/modals' import { useSearchParams } from 'next/navigation' -import { SWRConfig } from 'swr' -import type { NextPage, GetServerSidePropsContext } from 'next/types' import { Map } from '@/components/Map' import { AddButton } from '@/components/AddButton' import { NavbarContext } from '@/contexts/navbar' -const PageComponent: React.FC = () => { +export function MapPageContent() { const modals = useModals() const searchParams = useSearchParams() const { setDrawer } = useContext(NavbarContext) @@ -37,9 +37,7 @@ const PageComponent: React.FC = () => { centered: true, size: 'min(100%, 650px)', withCloseButton: false, - innerProps: { - - }, + innerProps: {}, } ) } @@ -50,44 +48,14 @@ const PageComponent: React.FC = () => { }, [setDrawer]) return ( - <> -
- - -
- - ) -} - -type MapPageProps = { - fallback: Record -} - -const MapPage: NextPage = ({ fallback }) => { - return ( - - - +
+ + +
) } - -export const getServerSideProps = async (_ctx: GetServerSidePropsContext) => { - return { - props: { - fallback: { - [`/api/submissions?limit=1000`]: { docs: [] } - } - } - } -} - -export default MapPage diff --git a/frontend/src/app/(map)/map/page.tsx b/frontend/src/app/(map)/map/page.tsx new file mode 100644 index 0000000..2844807 --- /dev/null +++ b/frontend/src/app/(map)/map/page.tsx @@ -0,0 +1,10 @@ +import { Suspense } from 'react' +import { MapPageContent } from './MapPageContent' + +export default function MapPage() { + return ( + + + + ) +} diff --git a/frontend/src/pages/api/indexFeedback.ts b/frontend/src/app/api/indexFeedback/route.ts similarity index 54% rename from frontend/src/pages/api/indexFeedback.ts rename to frontend/src/app/api/indexFeedback/route.ts index 0c16a55..3a2ad59 100644 --- a/frontend/src/pages/api/indexFeedback.ts +++ b/frontend/src/app/api/indexFeedback/route.ts @@ -1,20 +1,20 @@ -/* eslint-disable import/no-anonymous-default-export */ -import type { NextApiRequest, NextApiResponse } from 'next' +import { NextResponse } from 'next/server' -export default async (req: NextApiRequest, res: NextApiResponse) => { +export async function POST(request: Request) { try { + const body = await request.json() const reqPayload = await fetch(`${process.env.BACKEND_URL}/api/feedbackQuestions`, { method: 'post', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(req.body), + body: JSON.stringify(body), }) const data = await reqPayload.json() - res.status(200).json(data) + return NextResponse.json(data) } catch (e) { console.error('/api/indexFeedback', e) - res.status(400) + return NextResponse.json({ error: 'Request failed' }, { status: 400 }) } } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..d0ab38d --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,27 @@ +import { ColorSchemeScript } from '@mantine/core' +import { fontVar } from '@/fonts' +import { Providers } from './providers' + +import '@mantine/core/styles.css' + +import type { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'Мастер-план Сосновоборского городского округа', + description: 'Приветствуем вас на сайте, посвящённом разработке мастер-плана Соснового бора.', +} + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + {children} + + + + ) +} diff --git a/frontend/src/app/providers.tsx b/frontend/src/app/providers.tsx new file mode 100644 index 0000000..d62aa1a --- /dev/null +++ b/frontend/src/app/providers.tsx @@ -0,0 +1,38 @@ +'use client' + +import { SWRConfig } from 'swr' +import { MapProvider } from 'react-map-gl/maplibre' +import { MantineProvider } from '@mantine/core' +import { ModalsProvider } from '@mantine/modals' +import { FormContextProvider } from '@/contexts/form' +import { NavbarContextProvider } from '@/contexts/navbar' +import { IdeaModal } from '@/components/IdeaModal' +import { SurveyModal } from '@/components/SurveyModal' +import { theme } from '@/theme' + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + fetch(resource, init).then(res => res.json()), + }} + > + + + + + + {children} + + + + + + + ) +} diff --git a/frontend/src/components/IndexPage/HeroSection.tsx b/frontend/src/components/IndexPage/HeroSection.tsx new file mode 100644 index 0000000..614868b --- /dev/null +++ b/frontend/src/components/IndexPage/HeroSection.tsx @@ -0,0 +1,116 @@ +'use client' + +import { Box, Button, Group, Space, Stack, Text, Title } from '@mantine/core' +import Link from 'next/link' +import { useOpenSurveyModal } from '@/hooks/useOpenSurveyModal' +import { useMediaQuery } from '@mantine/hooks' + +export function HeroSection() { + const isMobile = useMediaQuery('(max-width: 768px)', true) + const isTablet = useMediaQuery('(max-width: 1024px)', true) + const openSurveyModal = useOpenSurveyModal() + + return ( + <> + + + {!isTablet &&
} + {!isTablet &&
} + + + МАСТЕР-ПЛАН РАЗВИТИЯ + <br /> + СОСНОВОГО БОРА + + + Уважаемые жители Соснового бора! Администрация городского округа совместно с госкорпорацией «Росатом» приступила к разработке Мастер-плана развития нашего города. Это масштабный проект, который определит принципы и направления развития Соснового бора на годы вперед. +

+ Чтобы план стал по-настоящему полезным и отразил интересы горожан, нам очень важно услышать ваше мнение. Что вас волнует? Что нужно улучшить в первую очередь? Каким вы видите Сосновый бор будущего? +

+ Поделитесь своими оценками, предложениями и пожеланиями в анонимной анкете. Это займет у вас не более 15 минут, а ваши искренние ответы станут основой для реальных изменений. +

+ Давайте вместе построим город, в котором хочется жить! +
+ + + + + + +
+ + + ) +} diff --git a/frontend/src/components/IndexPage/MapCTA.tsx b/frontend/src/components/IndexPage/MapCTA.tsx new file mode 100644 index 0000000..c5afe63 --- /dev/null +++ b/frontend/src/components/IndexPage/MapCTA.tsx @@ -0,0 +1,136 @@ +'use client' + +import { BackgroundImage, Box, Button, Group, Space, Stack, Text, Title } from '@mantine/core' +import Link from 'next/link' +import { useMediaQuery } from '@mantine/hooks' + +export function MapCTA() { + const isMobile = useMediaQuery('(max-width: 768px)', true) + const isTablet = useMediaQuery('(max-width: 1024px)', true) + + return ( + <> + + + {!isTablet && ( + <> +
+
+ + )} + + + + ПОДЕЛИТЕСЬ<br /> СВОИМ МНЕНИЕМ + + + + Выберите, что вы хотите отметить и укажите точку на карте, после чего оставьте комментарий
во всплывающем окне. +
+ + Идеи и предложения: Что может появиться в городе? Чего вам здесь не хватает? Что хочется изменить или наоборот, оставить как есть? + + + Проблемы: Что вас беспокоит в городе? Какие трудности встречаются? + + + + +
+
+ + + ) +} diff --git a/frontend/src/components/IndexPage/Sponsors.tsx b/frontend/src/components/IndexPage/Sponsors.tsx new file mode 100644 index 0000000..b3bf9ce --- /dev/null +++ b/frontend/src/components/IndexPage/Sponsors.tsx @@ -0,0 +1,69 @@ +'use client' + +import { Group, Image } from '@mantine/core' +import Link from 'next/link' +import { useMediaQuery } from '@mantine/hooks' + +const sponsors = [ + { + src: '/sponsors/sb.png', + href: 'https://sbor.ru/power/administration', + }, + { + src: '/sponsors/lenobl.png', + href: 'https://www.govvrn.ru/', + }, + { + src: '/sponsors/urbanika.svg', + href: 'https://www.urbanica.spb.ru', + }, + { + src: '/sponsors/rstm.svg', + href: 'https://www.rosenergoatom.ru/index.html', + }, + { + src: '/sponsors/rosatom.svg', + href: 'https://www.rosenergoatom.ru/index.html', + }, + { + src: '/sponsors/unit.svg', + href: 'https://unit4.io', + }, +] + +export function Sponsors() { + const isMobile = useMediaQuery('(max-width: 768px)', true) + + return ( + + {sponsors.map(x => ( + + {x.href} + + ))} + + ) +} diff --git a/frontend/src/components/IndexPage/SurveyCTA.tsx b/frontend/src/components/IndexPage/SurveyCTA.tsx new file mode 100644 index 0000000..66c024f --- /dev/null +++ b/frontend/src/components/IndexPage/SurveyCTA.tsx @@ -0,0 +1,113 @@ +'use client' + +import { Box, Button, Group, Space, Stack, Text, Title } from '@mantine/core' +import { useMediaQuery } from '@mantine/hooks' +import { useOpenSurveyModal } from '@/hooks/useOpenSurveyModal' + +export function SurveyCTA() { + const isMobile = useMediaQuery('(max-width: 768px)', true) + const isTablet = useMediaQuery('(max-width: 1024px)', true) + const openSurveyModal = useOpenSurveyModal() + + return ( + <> + + + {!isTablet &&
} + {!isTablet &&
} + + + + + ЧТО ВЫ ХОТИТЕ <br /> ИЗМЕНИТЬ? + + + + + + + Эта платформа создана для обсуждения перспектив развития с горожанами, экспертами, предпринимателями, представителями культурных и образовательных учреждений перспектив развития Соснового бора. +

+ Нам важно узнать, что волнует жителей города и как, по их мнению, должен развиваться Сосновый бор в будущем. Предложения и пожелания жителей будут учтены при разработке Мастер-плана. +

+ Чем больше жителей города предложат свои идеи и предложения по улучшению жизни в своем городе, или, наоборот, озвучат его актуальные проблемы – тем более реализуемым и полезным для каждого жителя получится итоговый документ. +

+ Прохождение анкеты займет около 15 минут, анонимно. +
+
+
+ + {isTablet && ( + + )} + + + ) +} diff --git a/frontend/src/components/IndexPage/TimelineSection.tsx b/frontend/src/components/IndexPage/TimelineSection.tsx new file mode 100644 index 0000000..33bfd55 --- /dev/null +++ b/frontend/src/components/IndexPage/TimelineSection.tsx @@ -0,0 +1,39 @@ +'use client' + +import { Image, Space, Stack, Title } from '@mantine/core' +import { useMediaQuery } from '@mantine/hooks' + +export function TimelineSection() { + const isMobile = useMediaQuery('(max-width: 768px)', true) + + return ( + <> + + + + + ГРАФИК ПРОЕКТА + + + + + ) +} diff --git a/frontend/src/components/SurveyModal/CheckboxWithOther.tsx b/frontend/src/components/SurveyModal/CheckboxWithOther.tsx index 5bb958f..21e7819 100644 --- a/frontend/src/components/SurveyModal/CheckboxWithOther.tsx +++ b/frontend/src/components/SurveyModal/CheckboxWithOther.tsx @@ -1,3 +1,5 @@ +'use client' + import { Fieldset, CheckboxGroup, Stack, Checkbox, TextInput, Text } from '@mantine/core' import { useState, useCallback } from 'react' import type { FieldValues, UseFormSetValue, UseFormWatch } from 'react-hook-form' diff --git a/frontend/src/fonts.ts b/frontend/src/fonts.ts new file mode 100644 index 0000000..0b31598 --- /dev/null +++ b/frontend/src/fonts.ts @@ -0,0 +1,3 @@ +import { Golos_Text } from 'next/font/google' + +export const fontVar = Golos_Text({ weight: ['400', '700'], subsets: ['latin', 'cyrillic'] }) diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx deleted file mode 100644 index 97169cc..0000000 --- a/frontend/src/pages/_app.tsx +++ /dev/null @@ -1,339 +0,0 @@ -import type { AppProps } from 'next/app' -import { AppShell, Box, Button, Center, Drawer, Flex, Group, MantineProvider, Stack, Text } from '@mantine/core' -import Link from 'next/link' -import { NextSeo } from 'next-seo' -import { useDisclosure, useMediaQuery } from '@mantine/hooks' -import { ModalsProvider } from '@mantine/modals' -import { IdeaModal } from '@/components/IdeaModal' -import { SurveyModal } from '@/components/SurveyModal' -import { MapProvider } from 'react-map-gl/maplibre' -import { useRouter } from 'next/router' -import { SWRConfig } from 'swr' -import { NavbarContext, NavbarContextProvider } from '@/contexts/navbar' -import { useContext } from 'react' -import { SubmissionFeed } from '@/components/SubmissionFeed' - -import '@mantine/core/styles.css' -import { FormContextProvider } from '@/contexts/form' - -import { theme, fontVar, mobileMenu, appShellStyles } from '@/theme' -import { navButtons } from '@/lib/navigation' -import { useOpenSurveyModal } from '@/hooks/useOpenSurveyModal' -import { Header } from '@/components/Header' - -const MapPageLayout = ({ children }: { children: React.ReactNode }) => { - const { drawer, setDrawer } = useContext(NavbarContext) - const isMobile = useMediaQuery('(max-width: 768px)', true) - const [mobileOpened, { toggle: toggleMobile }] = useDisclosure() - const openSurveyModal = useOpenSurveyModal() - - return ( - -
- - - - - - - - - - {navButtons.map(x => x.href ? ( - - ) : ( - - ))} - - - - - {children} - - - ) -} - -const PageLayout = ({ children }: { children: React.ReactNode }) => { - const [mobileOpened, { toggle: toggleMobile }] = useDisclosure() - const router = useRouter() - - const isMapPage = router.pathname == '/map' - - const isMobile = useMediaQuery('(max-width: 768px)') - const openSurveyModal = useOpenSurveyModal() - - if (isMapPage) { - return ( - - {children} - - ) - } - - return ( - -
- - - - - - СОСНОВЫЙ БОР - - - - - - - {navButtons.map(x => x.href ? ( - - ) : ( - - ))} - - - - - - -
- - {children} - -
- - - Мастер-план Сосновоборского городского округа - - - Copyright © 2026 design unit 4 & creators - - -
- - - ) -} - -export default function App({ Component, pageProps }: AppProps) { - return ( - <> - - fetch(resource, init).then(res => res.json()), - }} - > - - - - - - - - - - - - - - - - ) -} diff --git a/frontend/src/pages/_document.tsx b/frontend/src/pages/_document.tsx deleted file mode 100644 index aa8593a..0000000 --- a/frontend/src/pages/_document.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Head, Html, Main, NextScript } from 'next/document'; -import { ColorSchemeScript } from '@mantine/core'; - -export default function Document() { - return ( - - - - - -
- - - - ); -} \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx deleted file mode 100644 index 3011e11..0000000 --- a/frontend/src/pages/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { IndexPage } from '@/components/IndexPage' - -export default function Page() { - return ( - - ) -} - -// export const getStaticProps: GetStaticProps = async (ctx) => { -// const query = { -// limit: 1000, -// } -// let dataCms = await fetch( -// `${process.env.BACKEND_URL}/api/news${qs.stringify({ where: query }, { addQueryPrefix: true })}`, -// { -// method: 'get', -// }, -// ) -// .then(async res => await res.json()) -// .catch(err => console.log(err)) - -// const data = dataCms.docs.map(x => ({ -// id: x.id, -// image: x.image, -// title: x.title, -// })) - -// return { -// props: { -// data, -// }, -// } -// } \ No newline at end of file diff --git a/frontend/src/theme.ts b/frontend/src/theme.ts index 25a7d86..d167114 100644 --- a/frontend/src/theme.ts +++ b/frontend/src/theme.ts @@ -1,6 +1,6 @@ import { AppShell, Button, Fieldset, Group, Text, Title, createTheme } from '@mantine/core' -import { Golos_Text } from 'next/font/google' import type { MantineColorArray } from '@/types' +import { fontVar } from '@/fonts' import mobileMenu from './styles/mobileMenu.module.css' import groupStyles from './styles/group.module.css' @@ -9,9 +9,7 @@ import titleStyles from './styles/title.module.css' import buttonStyles from './styles/button.module.css' import appShellStyles from './styles/appShell.module.css' -export { mobileMenu, groupStyles, textStyles, titleStyles, buttonStyles, appShellStyles } - -export const fontVar = Golos_Text({ weight: ['400', '700'], subsets: ['latin', 'cyrillic'] }) +export { fontVar, mobileMenu, groupStyles, textStyles, titleStyles, buttonStyles, appShellStyles } export const createColorTuple = (color: string): MantineColorArray => [color, color, color, color, color, color, color, color, color, color] diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index c2a51c8..f827f75 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -25,12 +25,19 @@ "./*" ] }, - "target": "ES2017" + "target": "ES2017", + "plugins": [ + { + "name": "next" + } + ] }, "include": [ "next-env.d.ts", "**/*.ts", - "**/*.tsx" + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" ], "exclude": [ "node_modules" From a49352223ee911aab10e5c66ba5294c734cde53c Mon Sep 17 00:00:00 2001 From: Roman Timashev Date: Wed, 11 Feb 2026 01:52:35 +0700 Subject: [PATCH 05/17] index page upd --- .../components/IndexPage/HeroCTAButtons.tsx | 63 +++ .../IndexPage/HeroSection.module.css | 11 + .../src/components/IndexPage/HeroSection.tsx | 104 ++-- .../components/IndexPage/MapCTA.module.css | 19 + frontend/src/components/IndexPage/MapCTA.tsx | 112 ++--- .../components/IndexPage/Sponsors.module.css | 8 + .../src/components/IndexPage/Sponsors.tsx | 10 +- .../src/components/IndexPage/SurveyButton.tsx | 36 ++ .../components/IndexPage/SurveyCTA.module.css | 8 + .../src/components/IndexPage/SurveyCTA.tsx | 102 ++-- .../IndexPage/TimelineSection.module.css | 8 + .../components/IndexPage/TimelineSection.tsx | 28 +- frontend/src/components/IndexPage/index.tsx | 469 ------------------ 13 files changed, 306 insertions(+), 672 deletions(-) create mode 100644 frontend/src/components/IndexPage/HeroCTAButtons.tsx create mode 100644 frontend/src/components/IndexPage/HeroSection.module.css create mode 100644 frontend/src/components/IndexPage/MapCTA.module.css create mode 100644 frontend/src/components/IndexPage/Sponsors.module.css create mode 100644 frontend/src/components/IndexPage/SurveyButton.tsx create mode 100644 frontend/src/components/IndexPage/SurveyCTA.module.css create mode 100644 frontend/src/components/IndexPage/TimelineSection.module.css delete mode 100644 frontend/src/components/IndexPage/index.tsx diff --git a/frontend/src/components/IndexPage/HeroCTAButtons.tsx b/frontend/src/components/IndexPage/HeroCTAButtons.tsx new file mode 100644 index 0000000..5730c79 --- /dev/null +++ b/frontend/src/components/IndexPage/HeroCTAButtons.tsx @@ -0,0 +1,63 @@ +'use client' + +import { Button, Group } from '@mantine/core' +import Link from 'next/link' +import { useOpenSurveyModal } from '@/hooks/useOpenSurveyModal' + +export function HeroCTAButtons() { + const openSurveyModal = useOpenSurveyModal() + + return ( + + + + + + + + + + + ) +} diff --git a/frontend/src/components/IndexPage/HeroSection.module.css b/frontend/src/components/IndexPage/HeroSection.module.css new file mode 100644 index 0000000..8575f90 --- /dev/null +++ b/frontend/src/components/IndexPage/HeroSection.module.css @@ -0,0 +1,11 @@ +.heroTitle { + font-size: 32px; + line-height: 40px; + letter-spacing: 0.05em; +} +@media (min-width: $mantine-breakpoint-sm) { + .heroTitle { + font-size: 80px; + line-height: 86px; + } +} diff --git a/frontend/src/components/IndexPage/HeroSection.tsx b/frontend/src/components/IndexPage/HeroSection.tsx index 614868b..017d92c 100644 --- a/frontend/src/components/IndexPage/HeroSection.tsx +++ b/frontend/src/components/IndexPage/HeroSection.tsx @@ -1,15 +1,8 @@ -'use client' - -import { Box, Button, Group, Space, Stack, Text, Title } from '@mantine/core' -import Link from 'next/link' -import { useOpenSurveyModal } from '@/hooks/useOpenSurveyModal' -import { useMediaQuery } from '@mantine/hooks' +import { Box, Space, Stack, Text, Title } from '@mantine/core' +import { HeroCTAButtons } from './HeroCTAButtons' +import classes from './HeroSection.module.css' export function HeroSection() { - const isMobile = useMediaQuery('(max-width: 768px)', true) - const isTablet = useMediaQuery('(max-width: 1024px)', true) - const openSurveyModal = useOpenSurveyModal() - return ( <> @@ -20,34 +13,36 @@ export function HeroSection() { overflow: 'visible', }} > - {!isTablet &&
} - {!isTablet &&
} + +
+
+ МАСТЕР-ПЛАН РАЗВИТИЯ <br /> @@ -83,32 +74,7 @@ export function HeroSection() { <br /><br /> Давайте вместе построим город, в котором хочется жить! </Text> - <Group> - <Group - gap={30} - p={isMobile ? '24px 16px' : 2} - variant={isMobile ? undefined : 'noflip'} - > - <Button - component={Link} - href='/map' - size={isMobile ? 'xl' : 'md'} - w={isMobile ? '100%' : 'fit-content'} - bg={'secondary'} - > - Карта идей - </Button> - <Button - onClick={openSurveyModal} - size={isMobile ? 'xl' : 'md'} - w='fit-content' - bg={'secondary'} - c='primary' - > - Пройти опрос - </Button> - </Group> - </Group> + <HeroCTAButtons /> </Stack> </Box> </> diff --git a/frontend/src/components/IndexPage/MapCTA.module.css b/frontend/src/components/IndexPage/MapCTA.module.css new file mode 100644 index 0000000..b7bff68 --- /dev/null +++ b/frontend/src/components/IndexPage/MapCTA.module.css @@ -0,0 +1,19 @@ +.backgroundImage { + width: 100%; + background-size: contain; + aspect-ratio: 1240 / 762; +} +@media (max-width: $mantine-breakpoint-sm) { + .backgroundImage { + background-size: auto; + aspect-ratio: unset; + } +} + +.mapTitle { +} +@media (min-width: $mantine-breakpoint-sm) { + .mapTitle { + font-size: 42px !important; + } +} diff --git a/frontend/src/components/IndexPage/MapCTA.tsx b/frontend/src/components/IndexPage/MapCTA.tsx index c5afe63..190e419 100644 --- a/frontend/src/components/IndexPage/MapCTA.tsx +++ b/frontend/src/components/IndexPage/MapCTA.tsx @@ -1,16 +1,10 @@ -'use client' - import { BackgroundImage, Box, Button, Group, Space, Stack, Text, Title } from '@mantine/core' -import Link from 'next/link' -import { useMediaQuery } from '@mantine/hooks' +import classes from './MapCTA.module.css' export function MapCTA() { - const isMobile = useMediaQuery('(max-width: 768px)', true) - const isTablet = useMediaQuery('(max-width: 1024px)', true) - return ( <> - <Space h={isMobile ? 80 : 60} /> + <Space h={{ base: 80, sm: 60 }} /> <Box style={{ position: 'relative', @@ -20,60 +14,54 @@ export function MapCTA() { outline: 'solid 2px var(--mantine-color-secondary-1)', }} > - {!isTablet && ( - <> - <div style={{ - position: 'absolute', - top: '0%', - right: '100%', - width: '100%', - height: '100%', - background: 'url(/star.svg)', - backgroundRepeat: 'no-repeat', - aspectRatio: '153 / 150', - transform: 'scale(3)', - backgroundSize: 'contain', - backgroundPosition: 'right bottom', - maxWidth: '153px', - maxHeight: '150px', - zIndex: -1, - opacity: .25, - }} /> - <div style={{ - position: 'absolute', - top: '50%', - right: '-10%', - width: '100%', - height: '100%', - background: 'url(/star.svg)', - backgroundRepeat: 'no-repeat', - aspectRatio: '153 / 150', - backgroundSize: 'contain', - backgroundPosition: 'right bottom', - maxWidth: '153px', - maxHeight: '150px', - zIndex: -1, - opacity: .25, - }} /> - </> - )} + <Box visibleFrom='md'> + <div style={{ + position: 'absolute', + top: '0%', + right: '100%', + width: '100%', + height: '100%', + background: 'url(/star.svg)', + backgroundRepeat: 'no-repeat', + aspectRatio: '153 / 150', + transform: 'scale(3)', + backgroundSize: 'contain', + backgroundPosition: 'right bottom', + maxWidth: '153px', + maxHeight: '150px', + zIndex: -1, + opacity: .25, + }} /> + <div style={{ + position: 'absolute', + top: '50%', + right: '-10%', + width: '100%', + height: '100%', + background: 'url(/star.svg)', + backgroundRepeat: 'no-repeat', + aspectRatio: '153 / 150', + backgroundSize: 'contain', + backgroundPosition: 'right bottom', + maxWidth: '153px', + maxHeight: '150px', + zIndex: -1, + opacity: .25, + }} /> + </Box> <BackgroundImage src={'/indexMap.jpg'} bgr={'no-repeat'} pos={'relative'} - style={{ - width: '100%', - aspectRatio: !isMobile ? '1240 / 762' : undefined, - backgroundSize: isMobile ? 'auto' : 'contain', - }} + className={classes.backgroundImage} > <Stack align='flex-start' justify='center' variant='noflip' h='100%' - maw={isTablet ? '100%' : 'min(100%, 500px)'} - p={isTablet ? 24 : 38} + maw={{ base: '100%', md: 'min(100%, 500px)' }} + p={{ base: 24, md: 38 }} bg={'rgba(255,255,255, 0.9)'} style={{ boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.25)', @@ -83,11 +71,9 @@ export function MapCTA() { order={2} c={'primary'} mb={24} - ta={isTablet ? 'center' : undefined} + ta={{ base: 'center', md: undefined }} w={'100%'} - style={{ - fontSize: isMobile ? undefined : '42px', - }} + className={classes.mapTitle} > ПОДЕЛИТЕСЬ<br /> СВОИМ МНЕНИЕМ @@ -120,10 +106,20 @@ export function MapCTA() { mx={'auto'} > + diff --git a/frontend/src/components/IndexPage/Sponsors.module.css b/frontend/src/components/IndexPage/Sponsors.module.css new file mode 100644 index 0000000..b427bbb --- /dev/null +++ b/frontend/src/components/IndexPage/Sponsors.module.css @@ -0,0 +1,8 @@ +.sponsors { + gap: 40px; +} +@media (max-width: $mantine-breakpoint-sm) { + .sponsors { + gap: 80px; + } +} diff --git a/frontend/src/components/IndexPage/Sponsors.tsx b/frontend/src/components/IndexPage/Sponsors.tsx index b3bf9ce..c0573d8 100644 --- a/frontend/src/components/IndexPage/Sponsors.tsx +++ b/frontend/src/components/IndexPage/Sponsors.tsx @@ -1,8 +1,6 @@ -'use client' - import { Group, Image } from '@mantine/core' import Link from 'next/link' -import { useMediaQuery } from '@mantine/hooks' +import classes from './Sponsors.module.css' const sponsors = [ { @@ -32,11 +30,9 @@ const sponsors = [ ] export function Sponsors() { - const isMobile = useMediaQuery('(max-width: 768px)', true) - return ( {sponsors.map(x => ( + + + + ) +} diff --git a/frontend/src/components/IndexPage/SurveyCTA.module.css b/frontend/src/components/IndexPage/SurveyCTA.module.css new file mode 100644 index 0000000..563e9f7 --- /dev/null +++ b/frontend/src/components/IndexPage/SurveyCTA.module.css @@ -0,0 +1,8 @@ +.group { + gap: 8rem; +} +@media (max-width: $mantine-breakpoint-sm) { + .group { + gap: 4rem; + } +} diff --git a/frontend/src/components/IndexPage/SurveyCTA.tsx b/frontend/src/components/IndexPage/SurveyCTA.tsx index 66c024f..de6f5c6 100644 --- a/frontend/src/components/IndexPage/SurveyCTA.tsx +++ b/frontend/src/components/IndexPage/SurveyCTA.tsx @@ -1,17 +1,11 @@ -'use client' - -import { Box, Button, Group, Space, Stack, Text, Title } from '@mantine/core' -import { useMediaQuery } from '@mantine/hooks' -import { useOpenSurveyModal } from '@/hooks/useOpenSurveyModal' +import { Box, Group, Space, Stack, Text, Title } from '@mantine/core' +import { SurveyButton } from './SurveyButton' +import classes from './SurveyCTA.module.css' export function SurveyCTA() { - const isMobile = useMediaQuery('(max-width: 768px)', true) - const isTablet = useMediaQuery('(max-width: 1024px)', true) - const openSurveyModal = useOpenSurveyModal() - return ( <> - + - {!isTablet &&
} - {!isTablet &&
} + +
+
+ ЧТО ВЫ ХОТИТЕ <br /> ИЗМЕНИТЬ? - - - + - {isTablet && ( - - )} + + + ) diff --git a/frontend/src/components/IndexPage/TimelineSection.module.css b/frontend/src/components/IndexPage/TimelineSection.module.css new file mode 100644 index 0000000..e8ad10d --- /dev/null +++ b/frontend/src/components/IndexPage/TimelineSection.module.css @@ -0,0 +1,8 @@ +.stack { + gap: 0; +} +@media (max-width: $mantine-breakpoint-sm) { + .stack { + gap: 22px; + } +} diff --git a/frontend/src/components/IndexPage/TimelineSection.tsx b/frontend/src/components/IndexPage/TimelineSection.tsx index 33bfd55..59da0b7 100644 --- a/frontend/src/components/IndexPage/TimelineSection.tsx +++ b/frontend/src/components/IndexPage/TimelineSection.tsx @@ -1,17 +1,13 @@ -'use client' - -import { Image, Space, Stack, Title } from '@mantine/core' -import { useMediaQuery } from '@mantine/hooks' +import { Box, Image, Space, Stack, Title } from '@mantine/core' +import classes from './TimelineSection.module.css' export function TimelineSection() { - const isMobile = useMediaQuery('(max-width: 768px)', true) - return ( <> ГРАФИК ПРОЕКТА - + + + + + + ) diff --git a/frontend/src/components/IndexPage/index.tsx b/frontend/src/components/IndexPage/index.tsx deleted file mode 100644 index 9909038..0000000 --- a/frontend/src/components/IndexPage/index.tsx +++ /dev/null @@ -1,469 +0,0 @@ -'use client' - -import { BackgroundImage, Box, Group, Space, Stack, Text, Title, Image, Button } from '@mantine/core' -import Link from 'next/link' -import _ from '../../styles/index.module.css' -import { useMedia } from 'react-use' -import { useOpenSurveyModal } from '@/hooks/useOpenSurveyModal' - -const sponsors = [ - { - src: '/sponsors/sb.png', - href: 'https://sbor.ru/power/administration', - }, - { - src: '/sponsors/lenobl.png', - href: 'https://www.govvrn.ru/', - }, - { - src: '/sponsors/urbanika.svg', - href: 'https://www.urbanica.spb.ru', - }, - { - src: '/sponsors/rstm.svg', - href: 'https://www.rosenergoatom.ru/index.html', - }, - { - src: '/sponsors/rosatom.svg', - href: 'https://www.rosenergoatom.ru/index.html', - }, - { - src: '/sponsors/unit.svg', - href: 'https://unit4.io', - }, -] - -export function IndexPage() { - const isMobile = useMedia('(max-width: 768px)', true) - const isTablet = useMedia('(max-width: 1024px)', true) - const openSurveyModal = useOpenSurveyModal() - return ( - <> - - - {!isTablet &&
} - {!isTablet &&
} - - - МАСТЕР-ПЛАН РАЗВИТИЯ - <br /> - СОСНОВОГО БОРА - - - Уважаемые жители Соснового бора! Администрация городского округа совместно с госкорпорацией «Росатом» приступила к разработке Мастер-плана развития нашего города. Это масштабный проект, который определит принципы и направления развития Соснового бора на годы вперед. -

- Чтобы план стал по-настоящему полезным и отразил интересы горожан, нам очень важно услышать ваше мнение. Что вас волнует? Что нужно улучшить в первую очередь? Каким вы видите Сосновый бор будущего? -

- Поделитесь своими оценками, предложениями и пожеланиями в анонимной анкете. Это займет у вас не более 15 минут, а ваши искренние ответы станут основой для реальных изменений. -

- Давайте вместе построим город, в котором хочется жить! -
- - - - - - {/* - Опрос будет проходить с 23 ноября по 22 декабря - */} - -
- - - - - - - - ГРАФИК ПРОЕКТА - - - - - - - {!isTablet && ( - <> -
-
- - )} - - - - ПОДЕЛИТЕСЬ<br /> СВОИМ МНЕНИЕМ - - - - Выберите, что вы хотите отметить и укажите точку на карте, после чего оставьте комментарий
во всплывающем окне. -
- - Идеи и предложения: Что может появиться в городе? Чего вам здесь не хватает? Что хочется изменить или наоборот, оставить как есть? - - - Проблемы: Что вас беспокоит в городе? Какие трудности встречаются? - - - - -
-
- - - - - - {!isTablet &&
} - {!isTablet &&
} - - {/* {!isTablet && ( -
- )} */} - - - - ЧТО ВЫ ХОТИТЕ <br /> ИЗМЕНИТЬ? - - - - - - - Эта платформа создана для обсуждения перспектив развития с горожанами, экспертами, предпринимателями, представителями культурных и образовательных учреждений перспектив развития Соснового бора. -

- Нам важно узнать, что волнует жителей города и как, по их мнению, должен развиваться Сосновый бор в будущем. Предложения и пожелания жителей будут учтены при разработке Мастер-плана. -

- Чем больше жителей города предложат свои идеи и предложения по улучшению жизни в своем городе, или, наоборот, озвучат его актуальные проблемы – тем более реализуемым и полезным для каждого жителя получится итоговый документ. -

- Прохождение анкеты займет около 15 минут, анонимно. -
-
- - - {isTablet && ( - - )} - - - {sponsors.map(x => ( - - {x.href} - - )) - } - - - - ) -} From ae0ff50860766c8e3faf7f98752d313fc5602c86 Mon Sep 17 00:00:00 2001 From: Roman Timashev Date: Wed, 11 Feb 2026 02:12:25 +0700 Subject: [PATCH 06/17] fix map style load error --- frontend/src/components/MapMapbox/index.tsx | 8 ++++++-- frontend/src/components/SubmissionFeed/Item.tsx | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/MapMapbox/index.tsx b/frontend/src/components/MapMapbox/index.tsx index 6cff777..e8aebb4 100644 --- a/frontend/src/components/MapMapbox/index.tsx +++ b/frontend/src/components/MapMapbox/index.tsx @@ -1,6 +1,6 @@ 'use client' -import React from 'react' +import React, { useCallback, useState } from 'react' import { Map, GeolocateControl, NavigationControl } from 'react-map-gl/mapbox' import type { MapProps } from 'react-map-gl/mapbox' @@ -11,6 +11,9 @@ type MapMapboxProps = { } export default function MapMapbox({ children, onClick, initialViewState }: MapMapboxProps) { + const [isStyleLoaded, setIsStyleLoaded] = useState(false) + const handleLoad = useCallback(() => setIsStyleLoaded(true), []) + return ( - { children } + {isStyleLoaded && children} ) } diff --git a/frontend/src/components/SubmissionFeed/Item.tsx b/frontend/src/components/SubmissionFeed/Item.tsx index 9fda0b7..97508ba 100644 --- a/frontend/src/components/SubmissionFeed/Item.tsx +++ b/frontend/src/components/SubmissionFeed/Item.tsx @@ -3,7 +3,7 @@ import { Card, Stack, Text, Group, ScrollArea, ActionIcon } from '@mantine/core' import { useDisclosure } from '@mantine/hooks' import type { Submission } from '.' -import { useMap } from 'react-map-gl/maplibre' +import { useMap } from 'react-map-gl/mapbox' import { NavbarContext } from '@/contexts/navbar' import { useContext } from 'react' import { useSearchParams } from 'next/navigation' From 972f8b34b2955ac026a47c33f7a8e4730cb3c31e Mon Sep 17 00:00:00 2001 From: Roman Timashev Date: Wed, 11 Feb 2026 02:19:41 +0700 Subject: [PATCH 07/17] fix broken scroll to project cal --- frontend/src/app/(default)/layout.tsx | 10 +++++-- frontend/src/components/Header/index.tsx | 4 ++- frontend/src/components/IndexPage/MapCTA.tsx | 22 ++------------ .../src/components/IndexPage/MapCTAButton.tsx | 29 +++++++++++++++++++ frontend/src/lib/navigation.ts | 11 +++++++ 5 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 frontend/src/components/IndexPage/MapCTAButton.tsx diff --git a/frontend/src/app/(default)/layout.tsx b/frontend/src/app/(default)/layout.tsx index c3d77ef..7538322 100644 --- a/frontend/src/app/(default)/layout.tsx +++ b/frontend/src/app/(default)/layout.tsx @@ -6,7 +6,8 @@ import { useDisclosure, useMediaQuery } from '@mantine/hooks' import { useOpenSurveyModal } from '@/hooks/useOpenSurveyModal' import { Header } from '@/components/Header' import { mobileMenu, appShellStyles } from '@/theme' -import { navButtons } from '@/lib/navigation' +import { navButtons, scrollToHash } from '@/lib/navigation' +import type { MouseEvent } from 'react' export default function DefaultLayout({ children }: { children: React.ReactNode }) { const [mobileOpened, { toggle: toggleMobile }] = useDisclosure() @@ -71,7 +72,12 @@ export default function DefaultLayout({ children }: { children: React.ReactNode variant='subtle' c='primary' size='md' - onClick={toggleMobile} + onClick={(e: MouseEvent) => { + toggleMobile() + if (x.href!.includes('#')) { + scrollToHash(x.href!, e) + } + }} {...x.props} style={{ fontFamily: 'Nasalization, sans-serif', diff --git a/frontend/src/components/Header/index.tsx b/frontend/src/components/Header/index.tsx index 7027aa6..126ab51 100644 --- a/frontend/src/components/Header/index.tsx +++ b/frontend/src/components/Header/index.tsx @@ -3,7 +3,8 @@ import { AppShell, Burger, Button, Center, Flex, Group, Text } from '@mantine/core' import Link from 'next/link' import { useMediaQuery } from '@mantine/hooks' -import { navButtons } from '@/lib/navigation' +import { navButtons, scrollToHash } from '@/lib/navigation' +import type { MouseEvent } from 'react' export type HeaderProps = { height: number @@ -84,6 +85,7 @@ export function Header({ height, position, mobileOpened, toggleMobile, onSurveyC key={x.text} component={Link} href={x.href} + onClick={x.href.includes('#') ? (e: MouseEvent) => scrollToHash(x.href!, e) : undefined} variant='transparent' c={navButtonColor} styles={{ diff --git a/frontend/src/components/IndexPage/MapCTA.tsx b/frontend/src/components/IndexPage/MapCTA.tsx index 190e419..04d0ec5 100644 --- a/frontend/src/components/IndexPage/MapCTA.tsx +++ b/frontend/src/components/IndexPage/MapCTA.tsx @@ -1,5 +1,6 @@ -import { BackgroundImage, Box, Button, Group, Space, Stack, Text, Title } from '@mantine/core' +import { BackgroundImage, Box, Group, Space, Stack, Text, Title } from '@mantine/core' import classes from './MapCTA.module.css' +import { MapCTAButton } from './MapCTAButton' export function MapCTA() { return ( @@ -105,24 +106,7 @@ export function MapCTA() { p={6} mx={'auto'} > - - + diff --git a/frontend/src/components/IndexPage/MapCTAButton.tsx b/frontend/src/components/IndexPage/MapCTAButton.tsx new file mode 100644 index 0000000..4c29c01 --- /dev/null +++ b/frontend/src/components/IndexPage/MapCTAButton.tsx @@ -0,0 +1,29 @@ +'use client' + +import { Button } from '@mantine/core' +import Link from 'next/link' + +export function MapCTAButton() { + return ( + <> + + + + ) +} diff --git a/frontend/src/lib/navigation.ts b/frontend/src/lib/navigation.ts index dfe53ef..777f5b8 100644 --- a/frontend/src/lib/navigation.ts +++ b/frontend/src/lib/navigation.ts @@ -1,4 +1,5 @@ import type { Button } from '@mantine/core' +import type { MouseEvent } from 'react' export type NavButton = { text: string @@ -20,3 +21,13 @@ export const navButtons: NavButton[] = [ href: null, }, ] + +export function scrollToHash(href: string, e: MouseEvent) { + const hash = href.split('#')[1] + if (!hash) return + const el = document.getElementById(hash) + if (el) { + e.preventDefault() + el.scrollIntoView({ behavior: 'smooth' }) + } +} From 216b615d144ca0a1e02defa3cfc32ab99a21c39d Mon Sep 17 00:00:00 2001 From: Roman Timashev Date: Wed, 11 Feb 2026 02:52:34 +0700 Subject: [PATCH 08/17] fix typo --- frontend/src/app/(map)/map/MapPageContent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/(map)/map/MapPageContent.tsx b/frontend/src/app/(map)/map/MapPageContent.tsx index 0588788..38d6223 100644 --- a/frontend/src/app/(map)/map/MapPageContent.tsx +++ b/frontend/src/app/(map)/map/MapPageContent.tsx @@ -16,7 +16,7 @@ export function MapPageContent() { const previewFeature = Boolean(preview) const coords = preview?.split(',') - const [initalCoords] = useState(previewFeature && coords + const [initialCoords] = useState(previewFeature && coords ? { longitude: Number(coords[0]), latitude: Number(coords[1]), @@ -53,7 +53,7 @@ export function MapPageContent() { position: 'relative', }}>
From c1f7c1d1cd4879fdec241e9cb89c3663c9565628 Mon Sep 17 00:00:00 2001 From: Roman Timashev Date: Wed, 11 Feb 2026 02:52:41 +0700 Subject: [PATCH 09/17] html ru --- frontend/src/app/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index d0ab38d..7ab6a6f 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -13,7 +13,7 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + From e08b10d54e92586f99481ffda30b090ce392b621 Mon Sep 17 00:00:00 2001 From: Roman Timashev Date: Wed, 11 Feb 2026 02:52:47 +0700 Subject: [PATCH 10/17] use mapbox --- frontend/src/app/providers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/providers.tsx b/frontend/src/app/providers.tsx index d62aa1a..8403041 100644 --- a/frontend/src/app/providers.tsx +++ b/frontend/src/app/providers.tsx @@ -1,7 +1,7 @@ 'use client' import { SWRConfig } from 'swr' -import { MapProvider } from 'react-map-gl/maplibre' +import { MapProvider } from 'react-map-gl/mapbox' import { MantineProvider } from '@mantine/core' import { ModalsProvider } from '@mantine/modals' import { FormContextProvider } from '@/contexts/form' From 13aef5831367ee357a4d070bf314248336786317 Mon Sep 17 00:00:00 2001 From: Roman Timashev Date: Wed, 11 Feb 2026 02:53:05 +0700 Subject: [PATCH 11/17] fixes --- frontend/src/app/(map)/layout.tsx | 4 ++-- frontend/src/app/(map)/map/page.tsx | 3 ++- frontend/src/components/IndexPage/MapCTA.module.css | 2 -- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/(map)/layout.tsx b/frontend/src/app/(map)/layout.tsx index f9210e4..94f63a2 100644 --- a/frontend/src/app/(map)/layout.tsx +++ b/frontend/src/app/(map)/layout.tsx @@ -38,7 +38,7 @@ export default function MapLayout({ children }: { children: React.ReactNode }) { overflow: 'hidden', }} > - + @@ -91,7 +91,7 @@ export default function MapLayout({ children }: { children: React.ReactNode }) { ) : ( diff --git a/frontend/src/components/Map/index.tsx b/frontend/src/components/Map/index.tsx index 751d997..4719931 100644 --- a/frontend/src/components/Map/index.tsx +++ b/frontend/src/components/Map/index.tsx @@ -30,7 +30,10 @@ export function Map({ initialCoords }: MapProps) { { method: 'get', } - ).then(async res => await res.json()) + ).then(async res => { + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return await res.json() + }) ) const modals = useModals() @@ -68,7 +71,7 @@ export function Map({ initialCoords }: MapProps) { const isPreview = Boolean(searchParams.get('preview')) const features: Submission[] = (data?.items ?? []) - .filter((x: Submission) => x?.feature && JSON.stringify(x?.feature) !== '{}') + .filter((x: Submission) => x?.feature?.geometry?.coordinates?.length === 2) return ( {(!isLoading && !error && data) && (!addMode) && features - .map((x, i) => ( + .map((x) => ( { setText(states.error) - console.log(e) + console.error('Comment submission failed:', e) }) } @@ -77,12 +77,12 @@ export function CommentForm({ id, mutate }: CommentFormProps) {