Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ mise run stop # Stop dev containers and remove volumes

### Frontend (`frontend/src/`)

**Routing**: Next.js Pages Router (`pages/`). Key pages: `/` (landing), `/map` (interactive map), `/debug` (env debug).
**Routing**: Next.js App Router (`app/`). Key pages: `(default)/page.tsx` (landing), `(map)/map/page.tsx` (interactive map), `(default)/debug/page.tsx` (env debug).

**Styling**: Mantine UI 7.x component library + CSS Modules. PostCSS configured with `postcss-preset-mantine`. Custom theme defined in `_app.tsx` with project colors (primary orange `rgb(233 79 43)`, secondary green `rgb(155 185 98)`).
**Styling**: Mantine UI 7.x component library + CSS Modules. PostCSS configured with `postcss-preset-mantine`. Custom theme defined in `theme.ts` with project colors (primary orange `rgb(233 79 43)`, secondary green `rgb(155 185 98)`).

**State Management**: React Context API — `FormContext` (form/map interaction state), `NavbarContext` (sidebar/drawer toggle), `ClientIdContext` (anonymous client fingerprinting).

Expand Down
3,006 changes: 2,187 additions & 819 deletions frontend/package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"type-check": "tsc --noEmit",
"test": "vitest run",
"validate": "npm run type-check && npm run lint",
"api": " cd ../backend && npm run dev"
},
Expand Down Expand Up @@ -44,6 +45,7 @@
"postcss": "^8.4.33",
"postcss-preset-mantine": "^1.13.0",
"postcss-simple-vars": "^7.0.1",
"typescript": "^5"
"typescript": "^5",
"vitest": "^4.0.18"
}
}
Original file line number Diff line number Diff line change
@@ -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 (
<pre>
{debug}
{log.join('\n')}
</pre>
)
}
170 changes: 170 additions & 0 deletions frontend/src/app/(default)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
'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, scrollToHash } from '@/lib/navigation'
import type { MouseEvent } from 'react'

export default function DefaultLayout({ children }: { children: React.ReactNode }) {
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure()
const isMobile = useMediaQuery('(max-width: 768px)')
const openSurveyModal = useOpenSurveyModal()

return (
<AppShell
header={{
height: 0,
}}
>
<Header
height={0}
position='sticky'
mobileOpened={mobileOpened}
toggleMobile={toggleMobile}
onSurveyClick={openSurveyModal}
/>

<Drawer
withCloseButton={false}
opened={mobileOpened}
onClose={toggleMobile}
classNames={{
close: mobileMenu.close,
header: mobileMenu.header,
}}
styles={{
body: {
padding: 0,
}
}}
>
<Drawer.Header
p='14px 26px'
>
<Flex gap={20} align={'center'}>
<Text
lh={'27px'}
fw={'700'}
style={{
fontFamily: 'Nasalization, sans-serif',
}}
variant='subtle'
component={Link}
href={'/'}
c={'primary'}
>
СОСНОВЫЙ БОР
</Text>
</Flex>
<Drawer.CloseButton />
</Drawer.Header>
<Drawer.Body>
<Stack>
{navButtons.map(x => x.href ? (
<Button
key={x.text}
component={Link}
href={x.href}
variant='subtle'
c='primary'
size='md'
onClick={(e: MouseEvent) => {
toggleMobile()
if (x.href!.includes('#')) {
scrollToHash(x.href!, e)
}
}}
{...x.props}
style={{
fontFamily: 'Nasalization, sans-serif',
outline: 'none',
}}
>
{x.text}
</Button>
) : (
<Button
key={x.text}
variant='subtle'
c='primary'
size='md'
onClick={() => {
toggleMobile()
openSurveyModal()
}}
{...x.props}
style={{
fontFamily: 'Nasalization, sans-serif',
outline: 'none',
}}
>
{x.text}
</Button>
))}
</Stack>
</Drawer.Body>
</Drawer>

<AppShell.Main
style={{
position: 'relative',
overflowX: 'hidden',
overflowY: 'hidden',
}}
className={appShellStyles.root}
>
<div className={appShellStyles.filter} />
<Box
w={'100%'}
maw={1440}
mx={'auto'}
px={{
base: 20,
sm: 100,
}}
>
{children}
</Box>
<Center py={36}
style={{
position: 'relative',
zIndex: 1,
}}
>
<Group
w={'100%'}
maw={1440}
px={{ base: 20, lg: 100 }}
py={{ base: 26, lg: 54 }}
justify={'space-between'}
wrap='wrap'
>
<Text
fs={'16px'}
lh={'20px'}
fw={'500'}
c='white'
>
Мастер-план Сосновоборского городского округа
</Text>
<Text
fs={'16px'}
lh={'20px'}
fw={'500'}
c='white'
style={{
textAlign: isMobile ? 'center' : undefined,
}}
>
Copyright © 2026 design unit 4 & creators
</Text>
</Group>
</Center>
</AppShell.Main>
</AppShell>
)
}
17 changes: 17 additions & 0 deletions frontend/src/app/(default)/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<HeroSection />
<TimelineSection />
<MapCTA />
<SurveyCTA />
<Sponsors />
</>
)
}
123 changes: 123 additions & 0 deletions frontend/src/app/(map)/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<AppShell
header={{
height: isMobile ? 108 : 160,
}}
aside={{ width: '100%', breakpoint: 'lg', collapsed: { desktop: true, mobile: !mobileOpened } }}
navbar={{ width: !drawer ? 400 : 0, breakpoint: 'lg', collapsed: { desktop: false, mobile: !drawer } }}
>
<Header
height={isMobile ? 108 : 160}
mobileOpened={mobileOpened}
toggleMobile={toggleMobile}
onSurveyClick={openSurveyModal}
/>

<AppShell.Navbar
component='aside'
style={{
transition: 'width 0.2s ease, transform 0.2s ease',
overflow: 'hidden',
}}
>
<Suspense fallback={null}>
<SubmissionFeed />
</Suspense>
</AppShell.Navbar>

<Button
onClick={() => setDrawer(!drawer)}
hiddenFrom='lg'
size='compact-xs'
c='secondary'
bg='white'
style={{
position: 'absolute',
top: '8rem',
left: 0,
zIndex: 10,
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.25)',
}}
>
Предложения
</Button>

<AppShell.Aside
component={'nav'}
style={{
background: 'transparent',
}}
>
<Stack
h={'100%'}
mr={0}
py='md'
bg='white'
style={{
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.25)',
}}
>
{navButtons.map(x => x.href ? (
<Button
key={x.href}
component={Link}
href={x.href}
variant='subtle'
c='primary'
style={{
outline: 'none',
}}
onClick={toggleMobile}
>
{x.text}
</Button>
) : (
<Button
key={x.text}
onClick={() => {
openSurveyModal()
toggleMobile()
}}
variant='subtle'
c='primary'
style={{
outline: 'none',
}}
>
{x.text}
</Button>
))}
</Stack>
</AppShell.Aside>

<AppShell.Main
pt={0}
style={{
display: 'flex',
justifyContent: 'stretch',
alignItems: 'stretch',
}}
>
{children}
</AppShell.Main>
</AppShell>
)
}
Loading