diff --git a/front/package-lock.json b/front/package-lock.json index 2b86f66..a60e9b8 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -8,7 +8,6 @@ "name": "front", "version": "0.0.0", "dependencies": { - "@types/leaflet": "^1.9.3", "bootstrap": "^5.3.0", "jwt-decode": "^3.1.2", "leaflet": "^1.9.4", @@ -22,6 +21,8 @@ }, "devDependencies": { "@faker-js/faker": "^8.0.2", + "@types/leaflet": "^1.9.3", + "@types/lodash": "^4.14.196", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@typescript-eslint/eslint-plugin": "^5.61.0", @@ -30,6 +31,7 @@ "eslint": "^8.44.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.1", + "lodash": "^4.17.21", "typescript": "^5.0.2", "vite": "^4.4.0" } @@ -1059,7 +1061,8 @@ "node_modules/@types/geojson": { "version": "7946.0.10", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", - "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" + "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==", + "dev": true }, "node_modules/@types/json-schema": { "version": "7.0.12", @@ -1071,10 +1074,17 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.3.tgz", "integrity": "sha512-Caa1lYOgKVqDkDZVWkto2Z5JtVo09spEaUt2S69LiugbBpoqQu92HYFMGUbYezZbnBkyOxMNPXHSgRrRY5UyIA==", + "dev": true, "dependencies": { "@types/geojson": "*" } }, + "node_modules/@types/lodash": { + "version": "4.14.196", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.196.tgz", + "integrity": "sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==", + "dev": true + }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -2417,6 +2427,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", diff --git a/front/package.json b/front/package.json index 163e525..cc9df15 100644 --- a/front/package.json +++ b/front/package.json @@ -12,7 +12,6 @@ "addFetchApiRoute": "bash utils/addFetchApiRoute.sh" }, "dependencies": { - "@types/leaflet": "^1.9.3", "bootstrap": "^5.3.0", "jwt-decode": "^3.1.2", "leaflet": "^1.9.4", @@ -26,6 +25,8 @@ }, "devDependencies": { "@faker-js/faker": "^8.0.2", + "@types/leaflet": "^1.9.3", + "@types/lodash": "^4.14.196", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@typescript-eslint/eslint-plugin": "^5.61.0", @@ -34,6 +35,7 @@ "eslint": "^8.44.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.1", + "lodash": "^4.17.21", "typescript": "^5.0.2", "vite": "^4.4.0" } diff --git a/front/src/api/dispose/index.ts b/front/src/api/dispose/index.ts new file mode 100644 index 0000000..de3fcf8 --- /dev/null +++ b/front/src/api/dispose/index.ts @@ -0,0 +1,19 @@ +import { API_URL } from '../../config' +import { TrashboxDispose, DisposeResponse } from './types' + +const composeDisposeURL = () => ( + API_URL + '/announcement/dispose?' +) + +const composeDisposeBody = (ann_id: number, trashbox: TrashboxDispose) => ( + JSON.stringify({ + ann_id, + trashbox, + }) +) + +const processDispose = (data: DisposeResponse): boolean => { + return data.Success +} + +export { composeDisposeURL, composeDisposeBody, processDispose } diff --git a/front/src/api/dispose/types.ts b/front/src/api/dispose/types.ts new file mode 100644 index 0000000..d16e857 --- /dev/null +++ b/front/src/api/dispose/types.ts @@ -0,0 +1,23 @@ +import { composeDisposeBody } from '.' +import { isObject } from '../../utils/types' +import { Trashbox } from '../trashbox/types' + +type TrashboxDispose = Omit<Trashbox, 'Categories' | 'Address'> & { Category: string } + +type DisposeParams = Parameters<typeof composeDisposeBody> + +type DisposeAnnParams = DisposeParams extends [ann_id: number, ...args: infer P] ? P : never + +type DisposeResponse = { + Success: boolean, +} + +const isDisposeResponse = (obj: unknown): obj is DisposeResponse => ( + isObject(obj, { + 'Success': 'boolean', + }) +) + +export type { TrashboxDispose, DisposeParams, DisposeAnnParams, DisposeResponse } + +export { isDisposeResponse } diff --git a/front/src/api/trashbox/types.ts b/front/src/api/trashbox/types.ts index 9d9461b..7458354 100644 --- a/front/src/api/trashbox/types.ts +++ b/front/src/api/trashbox/types.ts @@ -1,6 +1,7 @@ import { isArrayOf, isObject, isString } from '../../utils/types' type Trashbox = { + Name: string, Lat: number, Lng: number, Address: string, @@ -9,6 +10,7 @@ type Trashbox = { const isTrashbox = (obj: unknown): obj is Trashbox => ( isObject(obj, { + 'Name': 'string', 'Lat': 'number', 'Lng': 'number', 'Address': 'string', diff --git a/front/src/components/AnnouncementDetails.tsx b/front/src/components/AnnouncementDetails.tsx index 86c62aa..f0c0814 100644 --- a/front/src/components/AnnouncementDetails.tsx +++ b/front/src/components/AnnouncementDetails.tsx @@ -1,13 +1,15 @@ import { Modal, Button } from 'react-bootstrap' import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet' -import { CSSProperties } from 'react' +import { CSSProperties, useState } from 'react' import LineDot from './LineDot' import { categoryNames } from '../assets/category' -import { useBook, useRemoveAnnouncement } from '../hooks/api' +import { useBook, useDispose, useRemoveAnnouncement } from '../hooks/api' import { Announcement } from '../api/announcement/types' import { iconItem } from '../utils/markerIcons' import { useId } from '../hooks' +import SelectDisposalTrashbox from './SelectDisposalTrashbox' +import { LatLng } from 'leaflet' type AnnouncementDetailsProps = { close: () => void, @@ -27,26 +29,85 @@ const styles = { } as CSSProperties, } -function AnnouncementDetails({ close, refresh, announcement: { - id, name, category, bestBy, description, lat, lng, address, metro, bookedBy, userId, -} }: AnnouncementDetailsProps) { +const View = ({ + announcement: { name, category, bestBy, description, lat, lng, address, metro }, +}: { announcement: Announcement }) => ( + <> + <h1>{name}</h1> + + <span>{categoryNames[category]}</span> + <span className='m-2'>•</span>{/* dot */} + <span>Годен до {new Date(bestBy).toLocaleString('ru-RU')}</span> + + <p className='mb-3'>{description}</p> + + <MapContainer style={styles.map} center={[lat, lng]} zoom={16} > + <TileLayer + attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' + url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' + /> + + <Marker icon={iconItem} position={[lat, lng]}> + <Popup> + {address} + <br /> + <LineDot station={metro} /> {metro} + </Popup> + </Marker> + </MapContainer> + </> +) + +type ControlProps = { + closeRefresh: () => void, + announcement: Announcement, + showDispose: () => void +} + +function Control({ + closeRefresh, + announcement: { bookedBy, id, userId }, + showDispose +}: ControlProps) { const { handleBook, bookButton } = useBook() - const removeRefresh = () => { + const { handleRemove, removeButton } = useRemoveAnnouncement(closeRefresh) + + const myId = useId() + + return ( + <> + <p>Забронировали {bookedBy} чел.</p> + {(myId === userId) ? ( + <> + <Button variant='success' onClick={showDispose}>Утилизировать</Button> + <Button variant='success' onClick={() => void handleRemove(id)} {...removeButton} /> + </> + ) : ( + <Button variant='success' onClick={() => void handleBook(id)} {...bookButton} /> + )} + </> + ) +} + +function AnnouncementDetails({ + close, + refresh, + announcement, +}: AnnouncementDetailsProps) { + const closeRefresh = () => { close() refresh() } - const { handleRemove, removeButton } = useRemoveAnnouncement(removeRefresh) - - const myId = useId() + const [disposeShow, setDisposeShow] = useState(false) return ( <div className='modal' style={styles.container} > - <Modal.Dialog style={{ minWidth: '50vw' }}> + <Modal.Dialog centered className='modal-dialog'> <Modal.Header closeButton onHide={close}> <Modal.Title> Подробнее @@ -54,39 +115,26 @@ function AnnouncementDetails({ close, refresh, announcement: { </Modal.Header> <Modal.Body> - <h1>{name}</h1> - - <span>{categoryNames[category]}</span> - <span className='m-2'>•</span>{/* dot */} - <span>Годен до {new Date(bestBy).toLocaleString('ru-RU')}</span> - - <p className='mb-3'>{description}</p> - - <MapContainer style={styles.map} center={[lat, lng]} zoom={16} > - <TileLayer - attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' - url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' - /> - - <Marker icon={iconItem} position={[lat, lng]}> - <Popup> - {address} - <br /> - <LineDot station={metro} /> {metro} - </Popup> - </Marker> - </MapContainer> + <View announcement={announcement} /> </Modal.Body> <Modal.Footer> - <p>Забронировали {bookedBy} чел.</p> - {(myId === userId) ? ( - <Button variant='success' onClick={() => void handleRemove(id)} {...removeButton} /> - ) : ( - <Button variant='success' onClick={() => void handleBook(id)} {...bookButton} /> - )} + <Control closeRefresh={closeRefresh} showDispose={() => setDisposeShow(true)} announcement={announcement} /> </Modal.Footer> </Modal.Dialog> + <Modal centered show={disposeShow} onHide={() => setDisposeShow(false)} style={{ zIndex: 100000 }}> + <Modal.Header closeButton> + <Modal.Title> + Утилизация + </Modal.Title> + </Modal.Header> + <SelectDisposalTrashbox + annId={announcement.id} + category={announcement.category} + address={new LatLng(announcement.lat, announcement.lng)} + closeRefresh={closeRefresh} + /> + </Modal> </div> ) } diff --git a/front/src/components/SelectDisposalTrashbox.tsx b/front/src/components/SelectDisposalTrashbox.tsx new file mode 100644 index 0000000..1bf4405 --- /dev/null +++ b/front/src/components/SelectDisposalTrashbox.tsx @@ -0,0 +1,119 @@ +import { Button, Modal } from 'react-bootstrap' +import { MapContainer, TileLayer } from 'react-leaflet' +import { CSSProperties, useState } from 'react' +import { LatLng } from 'leaflet' + +import { useDispose, useTrashboxes } from '../hooks/api' +import { UseFetchReturn, gotError, gotResponse } from '../hooks/useFetch' +import TrashboxMarkers from './TrashboxMarkers' +import { Category } from '../assets/category' +import { Trashbox } from '../api/trashbox/types' + +type SelectDisposalTrashboxProps = { + annId: number, + category: Category, + address: LatLng, + closeRefresh: () => void, +} + +type SelectedTrashbox = { + index: number, + category: string, +} + +const styles = { + map: { + width: '100%', + height: 400, + } as CSSProperties, +} + +function SelectDisposalTrashbox({ annId, category, address, closeRefresh }: SelectDisposalTrashboxProps) { + const trashboxes = useTrashboxes(address, category) + + const [selectedTrashbox, setSelectedTrashbox] = useState<SelectedTrashbox>({ index: -1, category: '' }) + + const { handleDispose, disposeButton } = useDispose(closeRefresh) + + return ( + <> + <Modal.Body> + <div className='mb-3'> + {gotResponse(trashboxes) + ? ( + gotError(trashboxes) ? ( + <p + style={styles.map} + className='text-danger' + >{trashboxes.error}</p> + ) : ( + <MapContainer + scrollWheelZoom={false} + style={styles.map} + center={address} + zoom={13} + className='' + > + <TileLayer + attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' + url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' + /> + <TrashboxMarkers + trashboxes={trashboxes.data} + selectTrashbox={setSelectedTrashbox} + /> + </MapContainer> + ) + ) : ( + <div style={styles.map}> + <p>Загрузка...</p> + </div> + ) + + } + </div> + <DisplaySelected trashboxes={trashboxes} selectedTrashbox={selectedTrashbox} /> + </Modal.Body> + <Modal.Footer> + <Button + {...disposeButton} + disabled={disposeButton.disabled || gotError(trashboxes) || !gotResponse(trashboxes) || selectedTrashbox.index < 0} + variant='success' + onClick={() => { + if (gotResponse(trashboxes) && !gotError(trashboxes)) { + const { Lat, Lng, Name } = trashboxes.data[selectedTrashbox.index] + void handleDispose(annId, { + Category: selectedTrashbox.category, + Lat, + Lng, + Name, + }) + } + }} + /> + </Modal.Footer> + </> + ) +} + +type DisplaySelectedProps = { + trashboxes: UseFetchReturn<Trashbox[]>, + selectedTrashbox: SelectedTrashbox, +} + +function DisplaySelected({ trashboxes, selectedTrashbox }: DisplaySelectedProps) { + if (gotResponse(trashboxes) && !gotError(trashboxes) && selectedTrashbox.index > -1) { + return ( + <> + <p className='mb-0'>Выбран пункт сбора мусора на {trashboxes.data[selectedTrashbox.index].Address}</p> + <p className='mb-0'>с категорией "{selectedTrashbox.category}"</p> + </> + ) + } + + return ( + <p className='mb-0'>Выберите пункт сбора мусора и категорию</p> + ) +} + +export default SelectDisposalTrashbox diff --git a/front/src/components/TrashboxMarkers.tsx b/front/src/components/TrashboxMarkers.tsx index d63dcb6..52a18bc 100644 --- a/front/src/components/TrashboxMarkers.tsx +++ b/front/src/components/TrashboxMarkers.tsx @@ -11,27 +11,32 @@ type TrashboxMarkersProps = { }) => void, } -function TrashboxMarkers({ trashboxes, selectTrashbox }: TrashboxMarkersProps) { - return ( - <>{trashboxes.map((trashbox, index) => ( - <Marker icon={iconTrash} key={`${trashbox.Lat}${trashbox.Lng}`} position={[trashbox.Lat, trashbox.Lng]}> - <Popup> - <p>{trashbox.Address}</p> - <p>Тип мусора: <> - {trashbox.Categories.map((category, j) => - <span key={trashbox.Address + category}> - <a href='#' onClick={() => selectTrashbox({ index, category })}> - {category} - </a> - {(j < trashbox.Categories.length - 1) ? ', ' : ''} - </span> - )} - </></p> - <p>{trashbox.Lat.toFixed(4)}, {trashbox.Lng.toFixed(4)}</p> - </Popup> - </Marker> - ))}</> - ) -} +const TrashboxMarkers = ({ trashboxes, selectTrashbox }: TrashboxMarkersProps) => ( + <>{trashboxes.map((trashbox, index) => ( + <Marker icon={iconTrash} key={`${trashbox.Lat}${trashbox.Lng}`} position={[trashbox.Lat, trashbox.Lng]}> + <Popup> + <p className='fw-bold m-0'>{trashbox.Name}</p> + <p className='m-0'>{trashbox.Address}</p> + <p>Тип мусора:{' '} + {trashbox.Categories.map((category, j) => + <span key={trashbox.Address + category}> + <a href='#' onClick={(e) => { + e.preventDefault() + e.stopPropagation() + selectTrashbox({ index, category }) + }}> + {category} + </a> + {(j < trashbox.Categories.length - 1) ? ', ' : ''} + </span> + )} + </p> + <p className='m-0'> + {trashbox.Lat.toFixed(4)}, {trashbox.Lng.toFixed(4)} + </p> + </Popup> + </Marker> + ))}</> +) export default TrashboxMarkers diff --git a/front/src/components/index.ts b/front/src/components/index.ts index 2edd60d..57f2928 100644 --- a/front/src/components/index.ts +++ b/front/src/components/index.ts @@ -13,3 +13,4 @@ export { default as StoriesPreview } from './StoriesPreview' export { default as Points } from './Points' export { default as SignOut } from './SignOut' export { default as Poetry } from './Poetry' +export { default as SelectDisposalTrashbox } from './SelectDisposalTrashbox' diff --git a/front/src/hooks/api/index.ts b/front/src/hooks/api/index.ts index 7931b1e..93062d8 100644 --- a/front/src/hooks/api/index.ts +++ b/front/src/hooks/api/index.ts @@ -8,3 +8,4 @@ export { default as useRemoveAnnouncement } from './useRemoveAnnouncement' export { default as useSignIn } from './useSignIn' export { default as useSignUp } from './useSignUp' export { default as usePoetry } from './usePoetry' +export { default as useDispose } from './useDispose' diff --git a/front/src/hooks/api/useDispose.ts b/front/src/hooks/api/useDispose.ts new file mode 100644 index 0000000..957e20b --- /dev/null +++ b/front/src/hooks/api/useDispose.ts @@ -0,0 +1,35 @@ +import { useCallback } from 'react' + +import { useSendWithButton } from '..' +import { composeDisposeBody, composeDisposeURL, processDispose } from '../../api/dispose' +import { DisposeParams, isDisposeResponse } from '../../api/dispose/types' + +const useDispose = (resolve: () => void) => { + const { doSend, button } = useSendWithButton( + 'Выбор сделан', + 'Зачтено', + true, + composeDisposeURL(), + 'POST', + true, + isDisposeResponse, + processDispose, + ) + + const doSendWithClose = useCallback(async (...args: DisposeParams) => { + const res = await doSend({}, { + body: composeDisposeBody(...args), + headers: { + 'Content-Type': 'application/json', + }, + }) + + if (res) { + resolve() + } + }, [doSend, resolve]) + + return { handleDispose: doSendWithClose, disposeButton: button } +} + +export default useDispose diff --git a/front/src/hooks/api/useRemoveAnnouncement.ts b/front/src/hooks/api/useRemoveAnnouncement.ts index 80865f6..ed9a92a 100644 --- a/front/src/hooks/api/useRemoveAnnouncement.ts +++ b/front/src/hooks/api/useRemoveAnnouncement.ts @@ -6,7 +6,7 @@ import { isRemoveAnnouncementResponse } from '../../api/removeAnnouncement/types const useRemoveAnnouncement = (resolve: () => void) => { const { doSend, button } = useSendWithButton( - 'Закрыть', + 'Закрыть объявление', 'Закрыто', true, composeRemoveAnnouncementURL(), diff --git a/front/src/hooks/api/useTrashboxes.ts b/front/src/hooks/api/useTrashboxes.ts index 80925e0..7e28027 100644 --- a/front/src/hooks/api/useTrashboxes.ts +++ b/front/src/hooks/api/useTrashboxes.ts @@ -1,18 +1,41 @@ import { LatLng } from 'leaflet' +import { sampleSize, random } from 'lodash' -import { useFetch } from '../' -import { composeTrashboxURL, processTrashbox } from '../../api/trashbox' -import { isTrashboxResponse } from '../../api/trashbox/types' +import { Trashbox } from '../../api/trashbox/types' +import { UseFetchReturn } from '../useFetch' -const useTrashboxes = (position: LatLng) => ( - useFetch( - composeTrashboxURL(position), - 'GET', - true, - isTrashboxResponse, - processTrashbox, - [], - ) +import { faker } from '@faker-js/faker/locale/ru' +import { Category, categories } from '../../assets/category' +import { useCallback, useMemo } from 'react' + +function genMockTrashbox(pos: LatLng): Trashbox { + const loc = faker.location.nearbyGPSCoordinate({ origin: [pos.lat, pos.lng], radius: 1 }) + + return { + Name: faker.company.name(), + Address: faker.location.streetAddress(), + Categories: faker.lorem.words({ max: 3, min: 1 }).split(' '), + Lat: loc[0], + Lng: loc[1], + } +} + +const useTrashboxes = (position: LatLng, category: Category): UseFetchReturn<Trashbox[]> => ( + // useFetch( + // composeTrashboxURL(position, category), + // 'GET', + // true, + // isTrashboxResponse, + // processTrashbox, + // [], + // ) + + { + data: useMemo(() => new Array(3).fill(3).map(() => genMockTrashbox(position)), [position]), + loading: false, + error: null, + refetch: () => { return }, + } ) export default useTrashboxes diff --git a/front/src/pages/AddPage.tsx b/front/src/pages/AddPage.tsx index 72c82a5..61c180d 100644 --- a/front/src/pages/AddPage.tsx +++ b/front/src/pages/AddPage.tsx @@ -4,11 +4,11 @@ import { MapContainer, TileLayer } from 'react-leaflet' import { latLng } from 'leaflet' import { useNavigate } from 'react-router-dom' -import { ClickHandler, LocationMarker, TrashboxMarkers } from '../components' -import { useAddAnnouncement, useTrashboxes } from '../hooks/api' +import { ClickHandler, LocationMarker } from '../components' +import { useAddAnnouncement } from '../hooks/api' import { categories, categoryNames } from '../assets/category' import { stations, lines, lineNames } from '../assets/metro' -import { fallbackError, gotError, gotResponse } from '../hooks/useFetch' +import { fallbackError, gotResponse } from '../hooks/useFetch' import { useOsmAddresses } from '../hooks/api' import CardLayout from '../components/CardLayout' @@ -22,9 +22,6 @@ const styles = { function AddPage() { const [addressPosition, setAddressPosition] = useState(latLng(59.972, 30.3227)) - const trashboxes = useTrashboxes(addressPosition) - const [selectedTrashbox, setSelectedTrashbox] = useState({ index: -1, category: '' }) - const address = useOsmAddresses(addressPosition) const { handleAdd, addButton } = useAddAnnouncement() @@ -117,6 +114,7 @@ function AddPage() { capture='environment' /> </Form.Group> + <Form.Group className='mb-3' controlId='metro'> <Form.Label> Станция метро @@ -136,50 +134,6 @@ function AddPage() { </Form.Select> </Form.Group> - <Form.Group className='mb-3' controlId='trashbox'> - <Form.Label>Пункт сбора мусора</Form.Label> - <div className='mb-3'> - {gotResponse(trashboxes) - ? ( - gotError(trashboxes) ? ( - <p - style={styles.map} - className='text-danger' - >{trashboxes.error}</p> - ) : ( - <MapContainer - scrollWheelZoom={false} - style={styles.map} - center={addressPosition} - zoom={13} - className='' - > - <TileLayer - attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' - url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' - /> - <TrashboxMarkers - trashboxes={trashboxes.data} - selectTrashbox={setSelectedTrashbox} - /> - </MapContainer> - ) - ) : ( - <div style={styles.map}> - <p>Загрузка...</p> - </div> - ) - } - </div> - {gotResponse(trashboxes) && !gotError(trashboxes) && selectedTrashbox.index > -1 ? ( - <p>Выбран пункт сбора мусора на { - trashboxes.data[selectedTrashbox.index].Address - } с категорией {selectedTrashbox.category}</p> - ) : ( - <p>Выберите пунк сбора мусора и категорию</p> - )} - </Form.Group> - <Button variant='success' type='submit' {...addButton} /> </Form> </CardLayout> diff --git a/front/src/utils/dispose.ts b/front/src/utils/dispose.ts new file mode 100644 index 0000000..65a87ff --- /dev/null +++ b/front/src/utils/dispose.ts @@ -0,0 +1,9 @@ +import { Announcement } from '../api/announcement/types' + +const DAY_MS = 24 * 60 * 60 * 1000 + +const isAnnExpired = (ann: Announcement) => ( + (ann.bestBy - Date.now()) < DAY_MS +) + +export { isAnnExpired }