Compare commits
2 Commits
vertical_c
...
e42293df3f
Author | SHA1 | Date | |
---|---|---|---|
e42293df3f | |||
0e7dd90646 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -28,7 +28,6 @@ dist-ssr
|
|||||||
*.db
|
*.db
|
||||||
uploads/
|
uploads/
|
||||||
.env
|
.env
|
||||||
poem_pic/
|
|
||||||
|
|
||||||
poem_pic/
|
poem_pic/
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ from . import auth_utils, orm_models, pydantic_schemas
|
|||||||
|
|
||||||
# Загружаем стихи
|
# Загружаем стихи
|
||||||
async def add_poems_to_db(async_db: AsyncSession):
|
async def add_poems_to_db(async_db: AsyncSession):
|
||||||
poems = []
|
|
||||||
f1 = open('poems.txt', encoding='utf-8', mode='r')#открыть фаил для чтения на русском
|
f1 = open('poems.txt', encoding='utf-8', mode='r')#открыть фаил для чтения на русском
|
||||||
for a in range(1, 110):
|
for a in range(1, 110):
|
||||||
f1.seek(0)#перейти к началу
|
f1.seek(0)#перейти к началу
|
||||||
@ -36,11 +35,8 @@ async def add_poems_to_db(async_db: AsyncSession):
|
|||||||
author += str1
|
author += str1
|
||||||
poem = orm_models.Poems(title=name, text=stixi, author=author)
|
poem = orm_models.Poems(title=name, text=stixi, author=author)
|
||||||
# В конце каждой итерации добавляем в базу данных
|
# В конце каждой итерации добавляем в базу данных
|
||||||
poems.append(poem)
|
async_db.add(poem)
|
||||||
|
async_db.commit()
|
||||||
async_db.add_all(poems)
|
|
||||||
|
|
||||||
await async_db.commit()
|
|
||||||
|
|
||||||
# close the file
|
# close the file
|
||||||
f1.close()
|
f1.close()
|
||||||
|
21
back/api.py
21
back/api.py
@ -200,6 +200,21 @@ def read_users_me(current_user: Annotated[pydantic_schemas.User, Depends(auth_ut
|
|||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
|
# ендпоинт для генерации refresh token. Генерируется при каждом входе юзера
|
||||||
|
# на сайт
|
||||||
|
@app.post("/api/token/refresh", response_model=pydantic_schemas.Token)
|
||||||
|
async def generate_refresh_token(
|
||||||
|
current_user: Annotated[pydantic_schemas.User, Depends(auth_utils.get_current_active_user)]
|
||||||
|
):
|
||||||
|
# задаем временной интервал, в течение которого токен можно использовать
|
||||||
|
access_token_expires = auth_utils.timedelta(minutes=auth_utils.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
# создаем новый токен токен
|
||||||
|
access_token = auth_utils.create_access_token(
|
||||||
|
data={"user_id": current_user.id}, expires_delta=access_token_expires
|
||||||
|
)
|
||||||
|
return {"access_token":access_token}
|
||||||
|
|
||||||
|
|
||||||
# изменяем рейтинг пользователя
|
# изменяем рейтинг пользователя
|
||||||
@app.post("/api/user/rating")
|
@app.post("/api/user/rating")
|
||||||
async def add_points(data: pydantic_schemas.AddRating, current_user: Annotated[pydantic_schemas.User, Depends(auth_utils.get_current_user)], db: Annotated[Session, Depends(auth_utils.get_session)]):
|
async def add_points(data: pydantic_schemas.AddRating, current_user: Annotated[pydantic_schemas.User, Depends(auth_utils.get_current_user)], db: Annotated[Session, Depends(auth_utils.get_session)]):
|
||||||
@ -267,18 +282,12 @@ async def get_trashboxes(data: pydantic_schemas.TrashboxRequest = Depends()): #
|
|||||||
'limit' : '1'
|
'limit' : '1'
|
||||||
}
|
}
|
||||||
# Перевод категории с фронта на категорию с сайта
|
# Перевод категории с фронта на категорию с сайта
|
||||||
try:
|
|
||||||
list_of_category = trashboxes_category[data.Category]
|
list_of_category = trashboxes_category[data.Category]
|
||||||
except:
|
|
||||||
list_of_category = trashboxes_category['other_things']
|
|
||||||
|
|
||||||
# Получение ответа от стороннего апи
|
# Получение ответа от стороннего апи
|
||||||
response = requests.post(TRASHBOXES_BASE_URL + "/nearest_recycling/get", headers=head, data=my_data, timeout=10)
|
response = requests.post(TRASHBOXES_BASE_URL + "/nearest_recycling/get", headers=head, data=my_data, timeout=10)
|
||||||
infos = response.json()
|
infos = response.json()
|
||||||
|
|
||||||
if 'error' in infos and infos['error_description'] == 'Invalid bearer token':
|
|
||||||
raise HTTPException(status_code=502, detail="Invalid trashboxes token")
|
|
||||||
|
|
||||||
# Чтение ответа
|
# Чтение ответа
|
||||||
trashboxes = []
|
trashboxes = []
|
||||||
for trashbox in infos["results"]:
|
for trashbox in infos["results"]:
|
||||||
|
@ -22,8 +22,7 @@
|
|||||||
"react-insta-stories": "^2.6.1",
|
"react-insta-stories": "^2.6.1",
|
||||||
"react-leaflet": "^4.2.1",
|
"react-leaflet": "^4.2.1",
|
||||||
"react-leaflet-custom-control": "^1.3.5",
|
"react-leaflet-custom-control": "^1.3.5",
|
||||||
"react-router-dom": "^6.14.1",
|
"react-router-dom": "^6.14.1"
|
||||||
"swiper": "^11.0.6"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^8.0.2",
|
"@faker-js/faker": "^8.0.2",
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 17 KiB |
Binary file not shown.
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
@ -105,7 +105,5 @@ const lineByName = (name: string) => (
|
|||||||
lines.find(line => stations[line].has(name))
|
lines.find(line => stations[line].has(name))
|
||||||
)
|
)
|
||||||
|
|
||||||
const DEFAULT_LINE = 'Петроградская'
|
|
||||||
|
|
||||||
export type { Lines }
|
export type { Lines }
|
||||||
export { lines, stations, colors, lineNames, lineByName, DEFAULT_LINE }
|
export { lines, stations, colors, lineNames, lineByName }
|
||||||
|
@ -1,73 +0,0 @@
|
|||||||
import { iconDormitory, iconITMO, iconLETI } from '../utils/markerIcons'
|
|
||||||
|
|
||||||
type LocationType = 'dormitory' | 'leti' | 'itmo'
|
|
||||||
|
|
||||||
const studentLocations: {
|
|
||||||
name: string,
|
|
||||||
position: [number, number],
|
|
||||||
type: LocationType
|
|
||||||
}[] = [
|
|
||||||
{
|
|
||||||
name: 'Первое, второе, третье общежития',
|
|
||||||
position: [59.987299, 30.330672],
|
|
||||||
type: 'dormitory',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Четвертое общежитие',
|
|
||||||
position: [59.985620, 30.331319],
|
|
||||||
type: 'dormitory',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Шестое общежитие',
|
|
||||||
position: [59.969713, 30.299851],
|
|
||||||
type: 'dormitory',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Седьмое общежитие',
|
|
||||||
position: [60.003723, 30.287616],
|
|
||||||
type: 'dormitory',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Восьмое общежитие',
|
|
||||||
position: [59.991115, 30.318752],
|
|
||||||
type: 'dormitory',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Общежития Межвузовского студенческого городка',
|
|
||||||
position: [59.871053, 30.307154],
|
|
||||||
type: 'dormitory',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Одиннадцатое общежитие',
|
|
||||||
position: [59.877962, 30.242889],
|
|
||||||
type: 'dormitory',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Общежитие Академии транспортных технологий',
|
|
||||||
position: [59.870375, 30.308646],
|
|
||||||
type: 'dormitory',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ЛЭТИ шестой корпус',
|
|
||||||
position: [59.971578, 30.296653],
|
|
||||||
type: 'leti',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ЛЭТИ Первый и другие корпуса',
|
|
||||||
position: [59.971947, 30.324303],
|
|
||||||
type: 'leti',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ИТМО',
|
|
||||||
position: [59.956363, 30.310029],
|
|
||||||
type: 'itmo',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const locationsIcons: Record<LocationType, L.Icon> = {
|
|
||||||
dormitory: iconDormitory,
|
|
||||||
itmo: iconITMO,
|
|
||||||
leti: iconLETI,
|
|
||||||
}
|
|
||||||
|
|
||||||
export { studentLocations, locationsIcons }
|
|
@ -11,7 +11,6 @@ import { iconItem } from '../utils/markerIcons'
|
|||||||
import { useId } from '../hooks'
|
import { useId } from '../hooks'
|
||||||
import SelectDisposalTrashbox from './SelectDisposalTrashbox'
|
import SelectDisposalTrashbox from './SelectDisposalTrashbox'
|
||||||
import StarRating from './StarRating'
|
import StarRating from './StarRating'
|
||||||
import StudentLocations from './StudentLocations'
|
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
container: {
|
container: {
|
||||||
@ -26,47 +25,33 @@ const styles = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ViewProps = {
|
type ViewProps = {
|
||||||
myId: number
|
myId: number,
|
||||||
announcement: Announcement
|
announcement: Announcement,
|
||||||
}
|
}
|
||||||
|
|
||||||
const View = ({
|
const View = ({
|
||||||
myId,
|
myId,
|
||||||
announcement: {
|
announcement: { name, category, bestBy, description, lat, lng, address, metro, userId },
|
||||||
name,
|
|
||||||
category,
|
|
||||||
bestBy,
|
|
||||||
description,
|
|
||||||
lat,
|
|
||||||
lng,
|
|
||||||
address,
|
|
||||||
metro,
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
}: ViewProps) => (
|
}: ViewProps) => (
|
||||||
<>
|
<>
|
||||||
<h1>{name}</h1>
|
<h1>{name}</h1>
|
||||||
|
|
||||||
<span>{categoryNames[category]}</span>
|
<span>{categoryNames[category]}</span>
|
||||||
<span className="m-2">•</span>
|
<span className='m-2'>•</span>{/* dot */}
|
||||||
{/* dot */}
|
|
||||||
<span>Годен до {bestBy}</span>
|
<span>Годен до {bestBy}</span>
|
||||||
|
|
||||||
<p className="mb-0">{description}</p>
|
<p className='mb-0'>{description}</p>
|
||||||
|
|
||||||
<p className="mb-3">
|
<p className='mb-3'>
|
||||||
Рейтинг пользователя:{' '}
|
Рейтинг пользователя: <StarRating dynamic={myId !== userId} userId={userId} />
|
||||||
<StarRating dynamic={myId !== userId} userId={userId} />
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<MapContainer style={styles.map} center={[lat, lng]} zoom={16} >
|
<MapContainer style={styles.map} center={[lat, lng]} zoom={16} >
|
||||||
<TileLayer
|
<TileLayer
|
||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StudentLocations />
|
|
||||||
|
|
||||||
<Marker icon={iconItem} position={[lat, lng]}>
|
<Marker icon={iconItem} position={[lat, lng]}>
|
||||||
<Popup>
|
<Popup>
|
||||||
{address}
|
{address}
|
||||||
@ -79,9 +64,9 @@ const View = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ControlProps = {
|
type ControlProps = {
|
||||||
myId: number
|
myId: number,
|
||||||
closeRefresh: () => void
|
closeRefresh: () => void,
|
||||||
announcement: Announcement
|
announcement: Announcement,
|
||||||
showDispose: () => void
|
showDispose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,44 +83,25 @@ function Control({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>Забронировали {bookedBy + (bookButton.disabled ? 1 : 0)} чел.</p>
|
<p>Забронировали {bookedBy + (bookButton.disabled ? 1 : 0)} чел.</p>
|
||||||
{myId === userId ? (
|
{(myId === userId) ? (
|
||||||
<div className="m-0">
|
<div className='m-0'>
|
||||||
<Button
|
<Button className='m-1' variant='success' onClick={showDispose}>Утилизировать</Button>
|
||||||
className="m-1"
|
<Button className='m-1' variant='success' onClick={() => void handleRemove(id)} {...removeButton} />
|
||||||
variant="success"
|
|
||||||
onClick={showDispose}
|
|
||||||
>
|
|
||||||
Утилизировать
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="m-1"
|
|
||||||
variant="success"
|
|
||||||
onClick={() => void handleRemove(id)}
|
|
||||||
{...removeButton}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button variant='success' onClick={() => void handleBook(id)} {...bookButton} />
|
||||||
variant="success"
|
|
||||||
onClick={() => void handleBook(id)}
|
|
||||||
{...bookButton}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnnouncementDetailsProps = {
|
type AnnouncementDetailsProps = {
|
||||||
className?: string
|
close: () => void,
|
||||||
show: boolean
|
refresh: () => void,
|
||||||
close: () => void
|
announcement: Announcement,
|
||||||
refresh: () => void
|
|
||||||
announcement: Announcement
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function AnnouncementDetails({
|
function AnnouncementDetails({
|
||||||
className,
|
|
||||||
show,
|
|
||||||
close,
|
close,
|
||||||
refresh,
|
refresh,
|
||||||
announcement,
|
announcement,
|
||||||
@ -150,16 +116,15 @@ function AnnouncementDetails({
|
|||||||
const myId = useId()
|
const myId = useId()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<div
|
||||||
centered
|
className='modal'
|
||||||
show={show}
|
style={styles.container}
|
||||||
onHide={close}
|
|
||||||
keyboard
|
|
||||||
className={className}
|
|
||||||
// backdrop={false}
|
|
||||||
>
|
>
|
||||||
|
<Modal.Dialog centered className='modal-dialog'>
|
||||||
<Modal.Header closeButton onHide={close}>
|
<Modal.Header closeButton onHide={close}>
|
||||||
<Modal.Title>Подробнее</Modal.Title>
|
<Modal.Title>
|
||||||
|
Подробнее
|
||||||
|
</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
|
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
@ -174,14 +139,12 @@ function AnnouncementDetails({
|
|||||||
announcement={announcement}
|
announcement={announcement}
|
||||||
/>
|
/>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
<Modal
|
</Modal.Dialog>
|
||||||
centered
|
<Modal centered show={disposeShow} onHide={() => setDisposeShow(false)} style={{ zIndex: 100000 }}>
|
||||||
show={disposeShow}
|
|
||||||
onHide={() => setDisposeShow(false)}
|
|
||||||
style={{ zIndex: 100000 }}
|
|
||||||
>
|
|
||||||
<Modal.Header closeButton>
|
<Modal.Header closeButton>
|
||||||
<Modal.Title>Утилизация</Modal.Title>
|
<Modal.Title>
|
||||||
|
Утилизация
|
||||||
|
</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<SelectDisposalTrashbox
|
<SelectDisposalTrashbox
|
||||||
annId={announcement.id}
|
annId={announcement.id}
|
||||||
@ -190,7 +153,7 @@ function AnnouncementDetails({
|
|||||||
closeRefresh={closeRefresh}
|
closeRefresh={closeRefresh}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Modal>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,13 +47,18 @@ const AuthForm = ({ goBack }: AuthFormProps) => {
|
|||||||
<Form.Control placeholder='Пароль' name='password' type='password' required />
|
<Form.Control placeholder='Пароль' name='password' type='password' required />
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<p>
|
<Form.Group className='mb-3' controlId='privacyPolicyConsent'>
|
||||||
Нажимая на кнопку, я даю своё согласие на обработку персональных данных и соглашаюсь с{' '}
|
<Form.Check>
|
||||||
|
<Form.Check.Input type='checkbox' required />
|
||||||
|
<Form.Check.Label>
|
||||||
|
Я согласен с{' '}
|
||||||
<a
|
<a
|
||||||
href={`${document.location.origin}/privacy_policy.pdf`}
|
href={`${document.location.origin}/privacy_policy.pdf`}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
>условиями политики конфиденциальности</a>
|
>условиями обработки персональных данных</a>
|
||||||
</p>
|
</Form.Check.Label>
|
||||||
|
</Form.Check>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
<ButtonGroup className='d-flex'>
|
<ButtonGroup className='d-flex'>
|
||||||
<Button
|
<Button
|
||||||
|
@ -77,7 +77,7 @@ function Filters({ filter, setFilter, filterShown, setFilterShown }: FiltersProp
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Button variant='success' type='submit'>
|
<Button variant='success' type='submit'>
|
||||||
Выбрать
|
Отправить
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
|
@ -24,10 +24,9 @@ function LocationMarker({ address, position, setPosition }: LocationMarkerProps)
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Marker icon={iconItem} position={position} zIndexOffset={1000}>
|
<Marker icon={iconItem} position={position}>
|
||||||
<Popup>
|
<Popup>
|
||||||
{address}
|
{address}
|
||||||
<br />
|
|
||||||
{position.lat.toFixed(4)}, {position.lng.toFixed(4)}
|
{position.lat.toFixed(4)}, {position.lng.toFixed(4)}
|
||||||
</Popup>
|
</Popup>
|
||||||
</Marker>
|
</Marker>
|
||||||
|
@ -26,7 +26,7 @@ function Poetry() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p><em>{poetry.data.author}</em></p>
|
<p><em>{poetry.data.author}</em></p>
|
||||||
<img className={styles.image} src={`/poem_pic/${poetry.data.id}.jpg`} alt='Иллюстрация' />
|
<img src={`/poem_pic/${poetry.data.id}.jpg`} alt='Иллюстрация' />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||||
|
import { Button } from 'react-bootstrap'
|
||||||
|
|
||||||
import { UserCategory, composeUserCategoriesFilters, userCategoriesInfos } from '../assets/userCategories'
|
import { UserCategory, composeUserCategoriesFilters, userCategoriesInfos } from '../assets/userCategories'
|
||||||
import { Announcement } from '../api/announcement/types'
|
import { Announcement } from '../api/announcement/types'
|
||||||
@ -95,9 +96,9 @@ function StoriesPreviewCarousel({ announcements, category }: StoriesPreviewProps
|
|||||||
|
|
||||||
return <div className={styles.container}>
|
return <div className={styles.container}>
|
||||||
{showScrollButtons.left &&
|
{showScrollButtons.left &&
|
||||||
<button onClick={doScroll(false)} className={`${styles.scrollButton} ${styles.leftScrollButton}`}>
|
<Button onClick={doScroll(false)} className={`${styles.scrollButton} ${styles.leftScrollButton}`}>
|
||||||
<img src={rightAngleIcon} alt='Показать ещё' />
|
<img src={rightAngleIcon} alt='Показать ещё' />
|
||||||
</button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
||||||
<ul className={styles.list} ref={ulElement}>
|
<ul className={styles.list} ref={ulElement}>
|
||||||
@ -105,9 +106,9 @@ function StoriesPreviewCarousel({ announcements, category }: StoriesPreviewProps
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{showScrollButtons.right &&
|
{showScrollButtons.right &&
|
||||||
<button onClick={doScroll(true)} className={`${styles.scrollButton} ${styles.rightScrollButton}`}>
|
<Button onClick={doScroll(true)} className={`${styles.scrollButton} ${styles.rightScrollButton}`}>
|
||||||
<img src={rightAngleIcon} alt='Показать ещё' />
|
<img src={rightAngleIcon} alt='Показать ещё' />
|
||||||
</button>
|
</Button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
import { Marker, Tooltip, useMap } from 'react-leaflet'
|
|
||||||
import { LatLng } from 'leaflet'
|
|
||||||
|
|
||||||
import { locationsIcons, studentLocations } from '../assets/studentLocations'
|
|
||||||
|
|
||||||
type StudentLocationsProps = {
|
|
||||||
setPosition?: (pos: LatLng) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
function StudentLocations({ setPosition }: StudentLocationsProps) {
|
|
||||||
const map = useMap()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>{
|
|
||||||
studentLocations.map((el) =>
|
|
||||||
<Marker
|
|
||||||
icon={locationsIcons[el.type]}
|
|
||||||
position={el.position}
|
|
||||||
eventHandlers={{
|
|
||||||
click: setPosition && (() => {
|
|
||||||
setPosition(new LatLng(...el.position))
|
|
||||||
map.setView(el.position)
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
{el.name}
|
|
||||||
</Tooltip>
|
|
||||||
</Marker>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StudentLocations
|
|
@ -17,4 +17,3 @@ export { default as SelectDisposalTrashbox } from './SelectDisposalTrashbox'
|
|||||||
export { default as StarRating } from './StarRating'
|
export { default as StarRating } from './StarRating'
|
||||||
export { default as CardLayout } from './CardLayout'
|
export { default as CardLayout } from './CardLayout'
|
||||||
export { default as LocateButton } from './LocateButton'
|
export { default as LocateButton } from './LocateButton'
|
||||||
export { default as StudentLocations } from './StudentLocations'
|
|
||||||
|
@ -8,68 +8,39 @@ function useStoryIndex(annLength: number | undefined) {
|
|||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
|
||||||
const withReset =
|
const withReset = <T>(f: SetState<T>) => (...args: Parameters<SetState<T>>) => {
|
||||||
<T>(f: SetState<T>) =>
|
|
||||||
(...args: Parameters<SetState<T>>) => {
|
|
||||||
setIndex(0)
|
setIndex(0)
|
||||||
setSearchParams(
|
setSearchParams(prev => ({
|
||||||
(prev) => ({
|
|
||||||
...Object.fromEntries(prev),
|
...Object.fromEntries(prev),
|
||||||
storyIndex: '0',
|
storyIndex: '0',
|
||||||
}),
|
}), { replace: true })
|
||||||
{ replace: true }
|
|
||||||
)
|
|
||||||
f(...args)
|
f(...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIndex(
|
setIndex(annLength ?
|
||||||
annLength
|
Number.parseInt(searchParams.get('storyIndex') || '0') :
|
||||||
? Number.parseInt(searchParams.get('storyIndex') || '0')
|
0)
|
||||||
: 0
|
|
||||||
)
|
|
||||||
// searchParams have actual query string at first render
|
// searchParams have actual query string at first render
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [annLength])
|
}, [annLength])
|
||||||
|
|
||||||
const increment = () =>
|
const increment = () => setIndex(prev => {
|
||||||
setIndex((prev) => {
|
|
||||||
const newIndex = (prev + 1) % (annLength || 1)
|
const newIndex = (prev + 1) % (annLength || 1)
|
||||||
setSearchParams(
|
setSearchParams(prev => ({
|
||||||
(prev) => ({
|
|
||||||
...Object.fromEntries(prev),
|
...Object.fromEntries(prev),
|
||||||
storyIndex: newIndex.toString(),
|
storyIndex: newIndex.toString(),
|
||||||
}),
|
}), { replace: true })
|
||||||
{ replace: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
return newIndex
|
return newIndex
|
||||||
})
|
})
|
||||||
|
|
||||||
const decrement = () =>
|
const decrement = () => setIndex(prev => {
|
||||||
setIndex((prev) => {
|
const newIndex = prev > 0 ? (prev - 1) : 0
|
||||||
const newIndex = prev > 0 ? prev - 1 : 0
|
setSearchParams(prev => ({
|
||||||
setSearchParams(
|
|
||||||
(prev) => ({
|
|
||||||
...Object.fromEntries(prev),
|
...Object.fromEntries(prev),
|
||||||
storyIndex: newIndex.toString(),
|
storyIndex: newIndex.toString(),
|
||||||
}),
|
}), { replace: true })
|
||||||
{ replace: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
return newIndex
|
|
||||||
})
|
|
||||||
|
|
||||||
const set = (n: number) =>
|
|
||||||
setIndex(() => {
|
|
||||||
const newIndex = n % (annLength || 1)
|
|
||||||
setSearchParams(
|
|
||||||
(prev) => ({
|
|
||||||
...Object.fromEntries(prev),
|
|
||||||
storyIndex: newIndex.toString(),
|
|
||||||
}),
|
|
||||||
{ replace: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
return newIndex
|
return newIndex
|
||||||
})
|
})
|
||||||
@ -79,7 +50,6 @@ function useStoryIndex(annLength: number | undefined) {
|
|||||||
withReset,
|
withReset,
|
||||||
increment,
|
increment,
|
||||||
decrement,
|
decrement,
|
||||||
set,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { CSSProperties, FormEventHandler, useEffect, useMemo, useState } from 'react'
|
import { CSSProperties, FormEventHandler, useEffect, useState } from 'react'
|
||||||
import { Form, Button } from 'react-bootstrap'
|
import { Form, Button } from 'react-bootstrap'
|
||||||
import { MapContainer, TileLayer } from 'react-leaflet'
|
import { MapContainer, TileLayer } from 'react-leaflet'
|
||||||
import { latLng } from 'leaflet'
|
import { latLng } from 'leaflet'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
import { MapClickHandler, LocationMarker, CardLayout, LocateButton, StudentLocations } from '../components'
|
import { MapClickHandler, LocationMarker, CardLayout, LocateButton } from '../components'
|
||||||
import { useAddAnnouncement } from '../hooks/api'
|
import { useAddAnnouncement } from '../hooks/api'
|
||||||
import { categories, categoryNames } from '../assets/category'
|
import { categories, categoryNames } from '../assets/category'
|
||||||
import { stations, lines, lineNames, DEFAULT_LINE } from '../assets/metro'
|
import { stations, lines, lineNames } from '../assets/metro'
|
||||||
import { fallbackError, gotResponse } from '../hooks/useFetch'
|
import { fallbackError, gotResponse } from '../hooks/useFetch'
|
||||||
import { useOsmAddresses } from '../hooks/api'
|
import { useOsmAddresses } from '../hooks/api'
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ function AddPage() {
|
|||||||
formData.append('address', address.data || '') // if address.error
|
formData.append('address', address.data || '') // if address.error
|
||||||
formData.set('bestBy', new Date((formData.get('bestBy') as number | null) || 0).getTime().toString())
|
formData.set('bestBy', new Date((formData.get('bestBy') as number | null) || 0).getTime().toString())
|
||||||
|
|
||||||
void handleAdd(formData)
|
handleAdd(formData)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -51,15 +51,15 @@ function AddPage() {
|
|||||||
<CardLayout text='Опубликовать объявление'>
|
<CardLayout text='Опубликовать объявление'>
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<Form.Group className='mb-3' controlId='name'>
|
<Form.Group className='mb-3' controlId='name'>
|
||||||
<Form.Label>Как ошляпите объявление?</Form.Label>
|
<Form.Label>Заголовок объявления</Form.Label>
|
||||||
<Form.Control type='text' required name='name' />
|
<Form.Control type='text' required name='name' />
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group className='mb-3' controlId='category'>
|
<Form.Group className='mb-3' controlId='category'>
|
||||||
<Form.Label>Какая категория?</Form.Label>
|
<Form.Label>Категория</Form.Label>
|
||||||
<Form.Select required name='category'>
|
<Form.Select required name='category'>
|
||||||
<option value='' hidden>
|
<option value='' hidden>
|
||||||
Выберите категорию предмета
|
Выберите категорию
|
||||||
</option>
|
</option>
|
||||||
{categories.map(category =>
|
{categories.map(category =>
|
||||||
<option key={category} value={category}>{categoryNames[category]}</option>
|
<option key={category} value={category}>{categoryNames[category]}</option>
|
||||||
@ -68,12 +68,12 @@ function AddPage() {
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group className='mb-3' controlId='bestBy'>
|
<Form.Group className='mb-3' controlId='bestBy'>
|
||||||
<Form.Label>Когда закрыть объявление?</Form.Label>
|
<Form.Label>Срок годности</Form.Label>
|
||||||
<Form.Control type='date' required name='bestBy' />
|
<Form.Control type='date' required name='bestBy' />
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group className='mb-3' controlId='address'>
|
<Form.Group className='mb-3' controlId='address'>
|
||||||
<Form.Label>Где забирать?</Form.Label>
|
<Form.Label>Адрес выдачи</Form.Label>
|
||||||
<div className='mb-3'>
|
<div className='mb-3'>
|
||||||
<MapContainer
|
<MapContainer
|
||||||
scrollWheelZoom={false}
|
scrollWheelZoom={false}
|
||||||
@ -86,8 +86,6 @@ function AddPage() {
|
|||||||
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
|
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{useMemo(() => <StudentLocations setPosition={setAddressPosition} />, [])}
|
|
||||||
|
|
||||||
<LocationMarker
|
<LocationMarker
|
||||||
address={gotResponse(address) ? fallbackError(address) : 'Загрузка...'}
|
address={gotResponse(address) ? fallbackError(address) : 'Загрузка...'}
|
||||||
position={addressPosition}
|
position={addressPosition}
|
||||||
@ -105,7 +103,7 @@ function AddPage() {
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group className='mb-3' controlId='description'>
|
<Form.Group className='mb-3' controlId='description'>
|
||||||
<Form.Label>Пару слов о вас и предмете?</Form.Label>
|
<Form.Label>Описание</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as='textarea'
|
as='textarea'
|
||||||
name='description'
|
name='description'
|
||||||
@ -115,7 +113,7 @@ function AddPage() {
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group className='mb-3' controlId='src'>
|
<Form.Group className='mb-3' controlId='src'>
|
||||||
<Form.Label>Загрузите гляделки? (фото или видео)</Form.Label>
|
<Form.Label>Иллюстрация (фото или видео)</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type='file'
|
type='file'
|
||||||
name='src'
|
name='src'
|
||||||
@ -126,9 +124,12 @@ function AddPage() {
|
|||||||
|
|
||||||
<Form.Group className='mb-3' controlId='metro'>
|
<Form.Group className='mb-3' controlId='metro'>
|
||||||
<Form.Label>
|
<Form.Label>
|
||||||
Где метро поближе?
|
Станция метро
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Form.Select name='metro' defaultValue={DEFAULT_LINE}>
|
<Form.Select name='metro'>
|
||||||
|
<option value=''>
|
||||||
|
Укажите ближайщую станцию метро
|
||||||
|
</option>
|
||||||
{lines.map(
|
{lines.map(
|
||||||
line =>
|
line =>
|
||||||
<optgroup key={line} label={lineNames[line]}>
|
<optgroup key={line} label={lineNames[line]}>
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { CSSProperties, useEffect, useMemo, useState } from 'react'
|
import { CSSProperties, useEffect, useMemo, useState } from 'react'
|
||||||
// import Stories from 'react-insta-stories'
|
import Stories from 'react-insta-stories'
|
||||||
// import { Story } from 'react-insta-stories/dist/interfaces'
|
import { Story } from 'react-insta-stories/dist/interfaces'
|
||||||
import { Swiper, SwiperSlide, useSwiper, useSwiperSlide } from 'swiper/react'
|
|
||||||
import { Virtual, Keyboard, Mousewheel, Zoom, Autoplay } from 'swiper/modules'
|
|
||||||
|
|
||||||
import { BottomNavBar, AnnouncementDetails, Filters } from '../components'
|
import { BottomNavBar, AnnouncementDetails, Filters } from '../components'
|
||||||
import { useFilters, useStoryDimensions, useStoryIndex } from '../hooks'
|
import { useFilters, useStoryDimensions, useStoryIndex } from '../hooks'
|
||||||
@ -11,149 +9,77 @@ import { Announcement } from '../api/announcement/types'
|
|||||||
import { categoryGraphics } from '../assets/category'
|
import { categoryGraphics } from '../assets/category'
|
||||||
import { UseFetchReturn, gotError } from '../hooks/useFetch'
|
import { UseFetchReturn, gotError } from '../hooks/useFetch'
|
||||||
|
|
||||||
import 'swiper/css'
|
|
||||||
import 'swiper/css/virtual'
|
|
||||||
import 'swiper/css/zoom'
|
|
||||||
import 'swiper/css/keyboard'
|
|
||||||
import 'swiper/css/mousewheel'
|
|
||||||
import 'swiper/css/autoplay'
|
|
||||||
|
|
||||||
import puffSpinner from '../assets/puff.svg'
|
import puffSpinner from '../assets/puff.svg'
|
||||||
import { SetState } from '../utils/types'
|
|
||||||
import { Button } from 'react-bootstrap'
|
function generateStories(announcements: Announcement[], refresh: () => void): Story[] {
|
||||||
import { throttle } from '../utils'
|
return announcements.map(announcement => {
|
||||||
|
return ({
|
||||||
|
id: announcement.id,
|
||||||
|
url: announcement.src || categoryGraphics[announcement.category],
|
||||||
|
type: announcement.src?.endsWith('mp4') ? 'video' : undefined,
|
||||||
|
seeMore: ({ close }: { close: () => void }) => (
|
||||||
|
<AnnouncementDetails close={close} refresh={refresh} announcement={announcement} />
|
||||||
|
),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackGenerateStories(announcements: UseFetchReturn<Announcement[]>) {
|
||||||
|
if (announcements.loading) {
|
||||||
|
return fallbackStory()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gotError(announcements)) {
|
||||||
|
return fallbackStory(announcements.error, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (announcements.data.length === 0) {
|
||||||
|
return [{ url: '/static/empty.png' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const stories = generateStories(announcements.data, announcements.refetch)
|
||||||
|
|
||||||
|
return stories
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackStory = (text = '', isError = false): Story[] => [{
|
||||||
|
content: ({ action }) => {
|
||||||
|
// ESLint can't detect that it is a component
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
useEffect(() => {
|
||||||
|
action('pause')
|
||||||
|
}, [action])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`m-auto ${isError ? 'text-danger' : ''}`}>
|
||||||
|
{text || <img src={puffSpinner} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
container: {
|
container: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: 'rgb(17, 17, 17)',
|
backgroundColor: 'rgb(17, 17, 17)',
|
||||||
height: 'calc(100dvh - 56px)',
|
|
||||||
} as CSSProperties,
|
} as CSSProperties,
|
||||||
detailsContainer: {
|
|
||||||
position: 'fixed',
|
|
||||||
top: 0,
|
|
||||||
zIndex: 1000,
|
|
||||||
textAlign: 'initial',
|
|
||||||
} as CSSProperties,
|
|
||||||
cardContainer: {
|
|
||||||
height: '100%',
|
|
||||||
margin: '0 auto',
|
|
||||||
position: 'relative',
|
|
||||||
} as CSSProperties,
|
|
||||||
cardMedia: {
|
|
||||||
height: '100%',
|
|
||||||
} as CSSProperties,
|
|
||||||
cardDetails: {
|
|
||||||
position: 'absolute',
|
|
||||||
width: '100%',
|
|
||||||
bottom: 0,
|
|
||||||
textAlign: 'center',
|
|
||||||
height: '56px',
|
|
||||||
boxShadow: 'inset 0px -50px 36px -28px rgba(0, 0, 0, 0.35)',
|
|
||||||
} as CSSProperties,
|
|
||||||
progressBar: {
|
|
||||||
height: '0.5dvh',
|
|
||||||
width: 'calc(100% - 2dvh)',
|
|
||||||
left: '1dvh',
|
|
||||||
position: 'absolute',
|
|
||||||
top: '1dvh',
|
|
||||||
zIndex: 1000,
|
|
||||||
} as CSSProperties,
|
|
||||||
}
|
|
||||||
|
|
||||||
type CardProps = {
|
|
||||||
announcement: Announcement
|
|
||||||
detailsShown: boolean
|
|
||||||
percentage: number
|
|
||||||
setDetailsShown: SetState<boolean>
|
|
||||||
}
|
|
||||||
|
|
||||||
const Card = ({
|
|
||||||
announcement,
|
|
||||||
detailsShown,
|
|
||||||
percentage,
|
|
||||||
setDetailsShown,
|
|
||||||
}: CardProps) => {
|
|
||||||
const swiperSlide = useSwiperSlide()
|
|
||||||
const swiper = useSwiper()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (detailsShown) swiper.autoplay.stop()
|
|
||||||
else swiper.autoplay.start()
|
|
||||||
}, [detailsShown])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AnnouncementDetails
|
|
||||||
className="swiper-no-swiping"
|
|
||||||
show={detailsShown && swiperSlide.isActive}
|
|
||||||
announcement={announcement}
|
|
||||||
close={() => setDetailsShown(false)}
|
|
||||||
refresh={() => {}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div style={styles.cardContainer}>
|
|
||||||
{swiperSlide.isActive && (
|
|
||||||
<div style={styles.progressBar}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
background: 'white',
|
|
||||||
height: '100%',
|
|
||||||
width: (1 - percentage) * 100 + '%',
|
|
||||||
transition: 'width 0.1s linear',
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="swiper-zoom-container">
|
|
||||||
{announcement.src?.endsWith('mp4') ||
|
|
||||||
announcement.src?.endsWith('webm') ? (
|
|
||||||
<video
|
|
||||||
style={styles.cardMedia}
|
|
||||||
src={announcement.src}
|
|
||||||
autoPlay={swiperSlide.isActive}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
style={styles.cardMedia}
|
|
||||||
src={
|
|
||||||
announcement.src ||
|
|
||||||
categoryGraphics[announcement.category]
|
|
||||||
}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div style={styles.cardDetails}>
|
|
||||||
<Button
|
|
||||||
variant="success"
|
|
||||||
onClick={() => setDetailsShown(true)}
|
|
||||||
>
|
|
||||||
Подробности
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function HomePage() {
|
function HomePage() {
|
||||||
const { height, width } = useStoryDimensions(16 / 9)
|
const { height, width } = useStoryDimensions(16 / 9)
|
||||||
|
|
||||||
const [filterShown, setFilterShown] = useState(false)
|
const [filterShown, setFilterShown] = useState(false)
|
||||||
const [detailsShown, setDetailsShown] = useState(false)
|
|
||||||
|
|
||||||
const [filter, setFilter] = useFilters()
|
const [filter, setFilter] = useFilters()
|
||||||
|
|
||||||
const announcements = useAnnouncements(filter)
|
const announcements = useAnnouncements(filter)
|
||||||
|
|
||||||
|
const stories = useMemo(() => fallbackGenerateStories(announcements), [announcements])
|
||||||
|
|
||||||
const index = useStoryIndex(announcements.data?.length)
|
const index = useStoryIndex(announcements.data?.length)
|
||||||
|
|
||||||
const [autoplayPercentage, setAutoplayPercentage] = useState(0)
|
return (<>
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Filters
|
<Filters
|
||||||
filter={filter}
|
filter={filter}
|
||||||
setFilter={index.withReset(setFilter)}
|
setFilter={index.withReset(setFilter)}
|
||||||
@ -161,62 +87,24 @@ function HomePage() {
|
|||||||
setFilterShown={setFilterShown}
|
setFilterShown={setFilterShown}
|
||||||
/>
|
/>
|
||||||
<div style={styles.container}>
|
<div style={styles.container}>
|
||||||
{!announcements.loading && !gotError(announcements) && (
|
<Stories
|
||||||
<Swiper
|
currentIndex={index.n % stories.length}
|
||||||
autoplay={{ delay: 11000, stopOnLastSlide: true }}
|
onStoryEnd={index.increment}
|
||||||
direction="vertical"
|
onNext={index.increment}
|
||||||
keyboard
|
onPrevious={index.decrement}
|
||||||
loop
|
stories={stories}
|
||||||
modules={[
|
defaultInterval={11000}
|
||||||
Autoplay,
|
height={height}
|
||||||
Keyboard,
|
width={width}
|
||||||
Mousewheel,
|
loop={true}
|
||||||
Virtual,
|
keyboardNavigation={true}
|
||||||
Zoom,
|
|
||||||
]}
|
|
||||||
mousewheel
|
|
||||||
onAutoplayTimeLeft={throttle(
|
|
||||||
(_: unknown, __: unknown, percentage: number) =>
|
|
||||||
setAutoplayPercentage(percentage),
|
|
||||||
100
|
|
||||||
)}
|
|
||||||
noSwiping={true}
|
|
||||||
onActiveIndexChange={(s) => index.set(s.activeIndex)}
|
|
||||||
slidesPerView={1}
|
|
||||||
spaceBetween={0}
|
|
||||||
virtual
|
|
||||||
zoom={{}}
|
|
||||||
initialSlide={index.n}
|
|
||||||
onZoomChange={(s, scale) => {
|
|
||||||
if (scale == 1) {
|
|
||||||
s.enable()
|
|
||||||
s.autoplay.start()
|
|
||||||
} else {
|
|
||||||
s.disable()
|
|
||||||
s.autoplay.stop()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{announcements.data.map((announcement, i) => (
|
|
||||||
<SwiperSlide
|
|
||||||
key={announcement.id}
|
|
||||||
virtualIndex={i}
|
|
||||||
zoom
|
|
||||||
>
|
|
||||||
<Card
|
|
||||||
announcement={announcement}
|
|
||||||
detailsShown={detailsShown}
|
|
||||||
percentage={autoplayPercentage}
|
|
||||||
setDetailsShown={setDetailsShown}
|
|
||||||
/>
|
/>
|
||||||
</SwiperSlide>
|
|
||||||
))}
|
|
||||||
</Swiper>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<BottomNavBar toggleFilters={setFilterShown} width={width} />
|
<BottomNavBar
|
||||||
</>
|
toggleFilters={setFilterShown}
|
||||||
)
|
width={width}
|
||||||
|
/>
|
||||||
|
</>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HomePage
|
export default HomePage
|
||||||
|
@ -48,11 +48,6 @@ function UserPage() {
|
|||||||
<div className='mb-3'>
|
<div className='mb-3'>
|
||||||
<Poetry />
|
<Poetry />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mb-3'>
|
|
||||||
<h4>Связаться с разработчиками</h4>
|
|
||||||
Канал в телеграме: <a href='https://t.me/porridger'>Porridger</a>
|
|
||||||
</div>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,3 @@
|
|||||||
.text {
|
.text {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
@ -42,7 +42,6 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
background: linear-gradient(to right, rgba(17, 17, 17, 0) 0%, rgba(17, 17, 17, 255) 100%);
|
background: linear-gradient(to right, rgba(17, 17, 17, 0) 0%, rgba(17, 17, 17, 255) 100%);
|
||||||
background-color: transparent;
|
|
||||||
display: block;
|
display: block;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 10%;
|
width: 10%;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const isAborted = <T>(err: Error): err is AbortError<T> =>
|
const isAborted = <T>(err: Error): err is AbortError<T> => (
|
||||||
err.name === 'AbortError'
|
err.name === 'AbortError'
|
||||||
|
)
|
||||||
|
|
||||||
type AbortErrorMessage = 'resent' | 'unmount' | 'cancel'
|
type AbortErrorMessage = 'resent' | 'unmount' | 'cancel'
|
||||||
|
|
||||||
@ -29,27 +30,4 @@ function handleHTTPErrors(res: Response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const throttle = (fn: Function, wait: number = 300) => {
|
export { isAborted, AbortError, handleHTTPErrors }
|
||||||
let inThrottle: boolean,
|
|
||||||
lastFn: ReturnType<typeof setTimeout>,
|
|
||||||
lastTime: number
|
|
||||||
return function (this: any) {
|
|
||||||
const context = this,
|
|
||||||
args = arguments
|
|
||||||
if (!inThrottle) {
|
|
||||||
fn.apply(context, args)
|
|
||||||
lastTime = Date.now()
|
|
||||||
inThrottle = true
|
|
||||||
} else {
|
|
||||||
clearTimeout(lastFn)
|
|
||||||
lastFn = setTimeout(() => {
|
|
||||||
if (Date.now() - lastTime >= wait) {
|
|
||||||
fn.apply(context, args)
|
|
||||||
lastTime = Date.now()
|
|
||||||
}
|
|
||||||
}, Math.max(wait - (Date.now() - lastTime), 0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { isAborted, AbortError, handleHTTPErrors, throttle }
|
|
||||||
|
@ -2,10 +2,6 @@ import L from 'leaflet'
|
|||||||
|
|
||||||
import itemMarker from '../assets/itemMarker.png'
|
import itemMarker from '../assets/itemMarker.png'
|
||||||
import trashMarker from '../assets/trashMarker.png'
|
import trashMarker from '../assets/trashMarker.png'
|
||||||
import dormitoryMarker from '../assets/dormitoryMarker.png'
|
|
||||||
import letiMarker from '../assets/letiMarker.png'
|
|
||||||
import itmoMarker from '../assets/itmoMarker.png'
|
|
||||||
|
|
||||||
|
|
||||||
const iconItem = new L.Icon({
|
const iconItem = new L.Icon({
|
||||||
iconUrl: itemMarker,
|
iconUrl: itemMarker,
|
||||||
@ -21,25 +17,4 @@ const iconTrash = new L.Icon({
|
|||||||
iconSize: [34, 41],
|
iconSize: [34, 41],
|
||||||
})
|
})
|
||||||
|
|
||||||
const iconDormitory = new L.Icon({
|
export { iconItem, iconTrash }
|
||||||
iconUrl: dormitoryMarker,
|
|
||||||
iconRetinaUrl: dormitoryMarker,
|
|
||||||
popupAnchor: [0, 0],
|
|
||||||
iconSize: [41, 41],
|
|
||||||
})
|
|
||||||
|
|
||||||
const iconLETI = new L.Icon({
|
|
||||||
iconUrl: letiMarker,
|
|
||||||
iconRetinaUrl: letiMarker,
|
|
||||||
popupAnchor: [0, 0],
|
|
||||||
iconSize: [41, 41],
|
|
||||||
})
|
|
||||||
|
|
||||||
const iconITMO = new L.Icon({
|
|
||||||
iconUrl: itmoMarker,
|
|
||||||
iconRetinaUrl: itmoMarker,
|
|
||||||
popupAnchor: [0, 0],
|
|
||||||
iconSize: [41, 41],
|
|
||||||
})
|
|
||||||
|
|
||||||
export { iconItem, iconTrash, iconDormitory, iconLETI, iconITMO }
|
|
||||||
|
Reference in New Issue
Block a user