From bc55ab8f684f7934949cf067dc163a2fe2e4bdd5 Mon Sep 17 00:00:00 2001 From: dm1sh Date: Tue, 18 Jul 2023 23:07:41 +0300 Subject: [PATCH 01/37] Converted api/announcements to use api/announcement processer as its part --- front/src/api/announcement/index.ts | 12 ++++++++++++ front/src/api/announcements/index.ts | 16 ++++------------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/front/src/api/announcement/index.ts b/front/src/api/announcement/index.ts index e69de29..d84eb44 100644 --- a/front/src/api/announcement/index.ts +++ b/front/src/api/announcement/index.ts @@ -0,0 +1,12 @@ +import { Announcement, AnnouncementResponse } from './types' + +const processAnnouncement = (data: AnnouncementResponse): Announcement => ({ + ...data, + lat: data.latitude, + lng: data.longtitude, + bestBy: data.best_by, + bookedBy: data.booked_by, + userId: data.user_id +}) + +export { processAnnouncement } diff --git a/front/src/api/announcements/index.ts b/front/src/api/announcements/index.ts index 2e7c3d7..9b5f79c 100644 --- a/front/src/api/announcements/index.ts +++ b/front/src/api/announcements/index.ts @@ -1,5 +1,6 @@ import { API_URL } from '../../config' import { FiltersType, URLEncodeFilters } from '../../utils/filters' +import { processAnnouncement } from '../announcement' import { Announcement } from '../announcement/types' import { AnnouncementsResponse } from './types' @@ -9,17 +10,8 @@ const composeAnnouncementsURL = (filters: FiltersType) => ( API_URL + '/announcements?' + new URLSearchParams(URLEncodeFilters(filters)).toString() ) -const processAnnouncements = (data: AnnouncementsResponse): Announcement[] => { - const annList = data.list_of_announcements - - return annList.map(ann => ({ - ...ann, - lat: ann.latitude, - lng: ann.longtitude, - bestBy: ann.best_by, - bookedBy: ann.booked_by, - userId: ann.user_id - })) -} +const processAnnouncements = (data: AnnouncementsResponse): Announcement[] => ( + data.list_of_announcements.map(processAnnouncement) +) export { initialAnnouncements, composeAnnouncementsURL, processAnnouncements } From 1b4eed529aa85115858a79deb599a30e957d190f Mon Sep 17 00:00:00 2001 From: dm1sh Date: Wed, 19 Jul 2023 23:23:01 +0300 Subject: [PATCH 02/37] Developed userPage prototype --- front/prototype.html | 315 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 front/prototype.html diff --git a/front/prototype.html b/front/prototype.html new file mode 100644 index 0000000..0eceeca --- /dev/null +++ b/front/prototype.html @@ -0,0 +1,315 @@ + + + + + + + Document + + + + +
+
+

Поэзия

+
+
"Fury said to
+
a mouse, That
+
he met
+
in the
+
house,
+
'Let us
+
both go
+
to law:
+
I will
+
prosecute
+
you.
+
Come, I'll
+
take no
+
denial;
+
We must
+
have a
+
trial:
+
For
+
really
+
this
+
morning
+
I've
+
nothing
+
to do.'
+
Said the
+
mouse to
+
the cur,
+
'Such a
+
trial,
+
dear sir,
+
With no
+
jury or
+
judge,
+
would be
+
wasting
+
our breath.'
+
'I'll be
+
judge,
+
I'll be
+
jury,'
+
Said
+
cunning
+
old Fury;
+
'I'll try
+
the whole
+
cause,
+
and
+
condemn
+
you
+
to
+
death.' "
+
+ +
+ + + + + + + + \ No newline at end of file From 7cf83d099d9f7a7922fc6516542091bf18392d1b Mon Sep 17 00:00:00 2001 From: dm1sh Date: Wed, 19 Jul 2023 23:24:58 +0300 Subject: [PATCH 03/37] Added api/user request prototype --- front/package-lock.json | 17 +++++++++++++++++ front/package.json | 1 + front/src/api/user/index.ts | 25 +++++++++++++++++++++++++ front/src/api/user/types.ts | 29 +++++++++++++++++++++++++++++ front/src/hooks/api/index.ts | 1 + front/src/hooks/api/useUser.ts | 22 ++++++++++++++++++++++ front/src/hooks/useFetch.ts | 2 ++ 7 files changed, 97 insertions(+) create mode 100644 front/src/api/user/index.ts create mode 100644 front/src/api/user/types.ts create mode 100644 front/src/hooks/api/useUser.ts diff --git a/front/package-lock.json b/front/package-lock.json index 096b2cd..52f0739 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -20,6 +20,7 @@ "react-router-dom": "^6.14.1" }, "devDependencies": { + "@faker-js/faker": "^8.0.2", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@typescript-eslint/eslint-plugin": "^5.61.0", @@ -817,6 +818,22 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@faker-js/faker": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.0.2.tgz", + "integrity": "sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", diff --git a/front/package.json b/front/package.json index 5420348..e848a0c 100644 --- a/front/package.json +++ b/front/package.json @@ -24,6 +24,7 @@ "react-router-dom": "^6.14.1" }, "devDependencies": { + "@faker-js/faker": "^8.0.2", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@typescript-eslint/eslint-plugin": "^5.61.0", diff --git a/front/src/api/user/index.ts b/front/src/api/user/index.ts new file mode 100644 index 0000000..f695a5f --- /dev/null +++ b/front/src/api/user/index.ts @@ -0,0 +1,25 @@ +import { API_URL } from '../../config' +import { UserResponse, User } from './types' + +import { faker } from '@faker-js/faker/locale/ru' + + +const initialUser: User = import.meta.env.DEV ? { // Temporary, until api is realized + id: Math.random() * 100, + name: faker.person.firstName() + ' ' + faker.person.lastName(), + regDate: faker.date.anytime().getTime(), +} : { + id: -1, + name: '', + regDate: 0, +} + +const composeUserURL = () => ( + API_URL + '/user?' +) + +const processUser = (data: UserResponse): User => { + return data +} + +export { initialUser, composeUserURL, processUser } diff --git a/front/src/api/user/types.ts b/front/src/api/user/types.ts new file mode 100644 index 0000000..8c9ee7e --- /dev/null +++ b/front/src/api/user/types.ts @@ -0,0 +1,29 @@ +import { isObject } from '../../utils/types' + +type User = { + id: number, + name: string, + regDate: number, +} + +const isUser = (obj: unknown): obj is User => ( + isObject(obj, { + 'id': 'number', + 'name': 'string', + 'regDate': 'number', + }) +) + +type UserResponse = User + +// const isUserResponse = (obj: unknown): obj is UserResponse => ( +// isObject(obj, { + +// }) +// ) + +const isUserResponse = isUser + +export type { UserResponse, User } + +export { isUserResponse, isUser } diff --git a/front/src/hooks/api/index.ts b/front/src/hooks/api/index.ts index 9023aee..ce87039 100644 --- a/front/src/hooks/api/index.ts +++ b/front/src/hooks/api/index.ts @@ -4,3 +4,4 @@ export { default as useAuth } from './useAuth' export { default as useTrashboxes } from './useTrashboxes' export { default as useAddAnnouncement } from './useAddAnnouncement' export { default as useOsmAddresses } from './useOsmAddress' +export { default as useUser } from './useUser' diff --git a/front/src/hooks/api/useUser.ts b/front/src/hooks/api/useUser.ts new file mode 100644 index 0000000..5531490 --- /dev/null +++ b/front/src/hooks/api/useUser.ts @@ -0,0 +1,22 @@ +import { initialUser } from '../../api/user' +import { User } from '../../api/user/types' +import { UseFetchErrored, UseFetchSucced } from '../useFetch' + +const useUser = (): UseFetchSucced | UseFetchErrored => ( + // useFetch( + // composeUserUrl(getToken()), + // 'GET', + // true, + // isUserResponse, + // processUser, + // initialUser + // ) + + { + data: initialUser, + loading: false, + error: null, + } +) + +export default useUser \ No newline at end of file diff --git a/front/src/hooks/useFetch.ts b/front/src/hooks/useFetch.ts index 41e9ab6..b401601 100644 --- a/front/src/hooks/useFetch.ts +++ b/front/src/hooks/useFetch.ts @@ -70,6 +70,8 @@ function useFetch( } } +export type { UseFetchErrored, UseFetchSucced } + export default useFetch export { gotError, fallbackError } From 7a044970f02d911a0addffa6d8c5a78f1ef3e411 Mon Sep 17 00:00:00 2001 From: dm1sh Date: Wed, 19 Jul 2023 23:25:25 +0300 Subject: [PATCH 04/37] Implemented UserPage --- front/src/App.css | 13 +-- front/src/assets/backArrow.svg | 4 + front/src/assets/rightAngle.svg | 4 + front/src/assets/userCategories.ts | 45 +++++++ front/src/components/BackHeader.tsx | 25 ++++ front/src/components/CategoryPreview.tsx | 25 ++++ front/src/components/StoriesPreview.tsx | 143 +++++++++++++++++++++++ front/src/components/index.ts | 3 + front/src/pages/UserPage.tsx | 23 +++- 9 files changed, 272 insertions(+), 13 deletions(-) create mode 100644 front/src/assets/backArrow.svg create mode 100644 front/src/assets/rightAngle.svg create mode 100644 front/src/assets/userCategories.ts create mode 100644 front/src/components/BackHeader.tsx create mode 100644 front/src/components/CategoryPreview.tsx create mode 100644 front/src/components/StoriesPreview.tsx diff --git a/front/src/App.css b/front/src/App.css index 9866f71..f28b5cb 100644 --- a/front/src/App.css +++ b/front/src/App.css @@ -1,16 +1,9 @@ -body { - height: 100vh; - overflow: hidden; - color: white; - font-family: sans-serif; -} - -.modal-content, .modal-content .form-select { - background-color: rgb(17, 17, 17) !important; +:root { + --bs-body-bg: rgb(17, 17, 17) !important; } /* В связи со сложившейся политической обстановкой */ .leaflet-attribution-flag { position: absolute; right: -100px; -} +} \ No newline at end of file diff --git a/front/src/assets/backArrow.svg b/front/src/assets/backArrow.svg new file mode 100644 index 0000000..b831a4a --- /dev/null +++ b/front/src/assets/backArrow.svg @@ -0,0 +1,4 @@ + + + diff --git a/front/src/assets/rightAngle.svg b/front/src/assets/rightAngle.svg new file mode 100644 index 0000000..2d6f9e5 --- /dev/null +++ b/front/src/assets/rightAngle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/front/src/assets/userCategories.ts b/front/src/assets/userCategories.ts new file mode 100644 index 0000000..c741c47 --- /dev/null +++ b/front/src/assets/userCategories.ts @@ -0,0 +1,45 @@ +import { Announcement } from '../api/announcement/types' +import { FiltersType } from '../utils/filters' + +const userCategories = ['givingOut', 'booked', 'history'] as const + +type UserCategory = typeof userCategories[number] + +const UserCategoriesNames: Record = { + givingOut: 'Раздача', + booked: 'Бронь', + history: 'История', +} + +const userCategoriesInfos: Record string> = { + givingOut: (ann: Announcement) => ( + `Годен до ${new Date(ann.bestBy).toLocaleDateString('ru')}` + ), + booked: (ann: Announcement) => ( + `Бронь ещё ${(ann as Announcement & { bookedBy: number[] }).bookedBy.length} чел.` + ), + history: (ann: Announcement) => ( + `Забрал ${new Date((ann as Announcement & { taken: number }).taken).toLocaleDateString('ru')}` + ), +} + +const composeUserCategoriesFilters: Record FiltersType> = { + givingOut: () => { + const userId = -1 + + return ({ userId }) + }, + booked: () => { + const userId = -1 + + return ({ bookedBy: userId }) + }, + history: () => { + const userId = -1 + + return ({ userId, status: 'taken' }) + } +} + +export type { UserCategory } +export { userCategories, UserCategoriesNames, userCategoriesInfos, composeUserCategoriesFilters } diff --git a/front/src/components/BackHeader.tsx b/front/src/components/BackHeader.tsx new file mode 100644 index 0000000..e8ba89f --- /dev/null +++ b/front/src/components/BackHeader.tsx @@ -0,0 +1,25 @@ +import { Link } from 'react-router-dom' +import { Navbar } from 'react-bootstrap' + +import BackButton from '../assets/backArrow.svg' + +type BackHeaderProps = { + text: string +} + +function BackHeader({ text }: BackHeaderProps) { + return ( + + + Go back + + +

+ {text} +

+
+
+ ) +} + +export default BackHeader diff --git a/front/src/components/CategoryPreview.tsx b/front/src/components/CategoryPreview.tsx new file mode 100644 index 0000000..caaecd7 --- /dev/null +++ b/front/src/components/CategoryPreview.tsx @@ -0,0 +1,25 @@ +import { StoriesPreview } from '.' +import { UserCategoriesNames, UserCategory, composeUserCategoriesFilters } from '../assets/userCategories' +import { useAnnouncements } from '../hooks/api' +import { gotError } from '../hooks/useFetch' + +type CategoryPreviewProps = { + category: UserCategory +} + +function CategoryPreview({ category }: CategoryPreviewProps) { + const announcements = useAnnouncements(composeUserCategoriesFilters[category]()) + + return ( +
+

{UserCategoriesNames[category]}

+ {gotError(announcements) ? ( +

{announcements.error}

+ ) : (announcements.loading ? 'Загрузка...' : + + )} +
+ ) +} + +export default CategoryPreview diff --git a/front/src/components/StoriesPreview.tsx b/front/src/components/StoriesPreview.tsx new file mode 100644 index 0000000..24b8ffe --- /dev/null +++ b/front/src/components/StoriesPreview.tsx @@ -0,0 +1,143 @@ +import { Link } from 'react-router-dom' +import { CSSProperties, useEffect, useMemo, useRef, useState } from 'react' + +import { UserCategory, userCategoriesInfos } from '../assets/userCategories' +import { Announcement } from '../api/announcement/types' +import { categoryGraphics, categoryNames } from '../assets/category' + +import rightAngleIcon from '../assets/rightAngle.svg' +import { Button } from 'react-bootstrap' + +type StoriesPreviewProps = { + announcements: Announcement[], + category: UserCategory, +} + +const styles = { + container: { + transform: 'translateX(0)', + } as CSSProperties, + ul: { + display: 'flex', + gap: 10, + listStyleType: 'none', + overflow: 'scroll', + paddingLeft: 0, + scrollBehavior: 'smooth', + } as CSSProperties, + link: { + textDecoration: 'none', + color: 'var(--bs-body-color)' + } as CSSProperties, + image: { + height: '25vh', + maxWidth: 'calc(25vh * 9 / 16)', + objectFit: 'contain', + borderRadius: 12, + marginBottom: 5, + } as CSSProperties, + title: { + overflow: 'hidden', + textOverflow: 'ellipsis', + marginBottom: 5, + } as CSSProperties, + scrollButton: { + position: 'fixed', + right: 0, + top: 0, + zIndex: 100, + background: 'linear-gradient(to right, rgba(17, 17, 17, 0) 0%, rgba(17, 17, 17, 255) 100%)', + display: 'block', + height: '100%', + width: '10%', + border: 'none', + cursor: 'default', + borderRadius: 0, + } as CSSProperties, + leftScrollButton: { + left: 0, + transform: 'scaleX(-1)' + } as CSSProperties, + rightScrollButton: { + right: 0, + } as CSSProperties, +} + +function StoriesPreview({ announcements, category }: StoriesPreviewProps) { + const ulElement = useRef(null) + const [showScrollButtons, setShowScrollButtons] = useState({ left: false, right: false }) + + const determineShowScrollButtons = (ul: HTMLUListElement) => ( + setShowScrollButtons({ + left: ul.scrollLeft > 0, + right: ul.scrollLeft < (ul.scrollWidth - ul.clientWidth), + }) + ) + + useEffect(() => { + const ul = ulElement.current + if (ul) { + determineShowScrollButtons(ul) + + const f = () => determineShowScrollButtons(ul) + + ul.addEventListener('scroll', f) + + return () => ul.removeEventListener('scroll', f) + } + }, []) + + useEffect(() => { + const ul = ulElement.current + + if (ul) { + determineShowScrollButtons(ul) + } + }, [announcements]) + + const doScroll = (forward: boolean) => () => { + const ul = ulElement.current + if (ul) { + const storyWidth = window.innerHeight * 0.25 * 9 / 16 + 10 + ul.scrollLeft += forward ? storyWidth : -storyWidth + } + } + + return
+ {showScrollButtons.left && + + } +
    + {useMemo(() => announcements.map((ann, i) => ( +
  • + + {ann.src?.endsWith('mp4') ? ( +
  • + )), [announcements, category])} +
+ {showScrollButtons.right && + + } +
+} + +export default StoriesPreview \ No newline at end of file diff --git a/front/src/components/index.ts b/front/src/components/index.ts index aa92c4b..6562e70 100644 --- a/front/src/components/index.ts +++ b/front/src/components/index.ts @@ -7,3 +7,6 @@ export { default as TrashboxMarkers } from './TrashboxMarkers' export { default as WithToken } from './WithToken' export { default as ClickHandler } from './ClickHandler' export { default as AuthForm } from './AuthForm' +export { default as BackHeader } from './BackHeader' +export { default as CategoryPreview } from './CategoryPreview' +export { default as StoriesPreview } from './StoriesPreview' diff --git a/front/src/pages/UserPage.tsx b/front/src/pages/UserPage.tsx index 6c90c41..a35a1be 100644 --- a/front/src/pages/UserPage.tsx +++ b/front/src/pages/UserPage.tsx @@ -1,9 +1,26 @@ -import { Link } from 'react-router-dom' +import { Container } from 'react-bootstrap' + +import BackHeader from '../components/BackHeader' +import { useUser } from '../hooks/api' +import { userCategories } from '../assets/userCategories' +import { CategoryPreview } from '../components' +import { gotError } from '../hooks/useFetch' function UserPage() { - /* TODO */ + const user = useUser() - return

For Yet Go Home

+ return ( + + + {userCategories.map(cat => ( + + ))} + + ) } export default UserPage From bc154f8b6b7999a39f15d4a8746ffd343a68e376 Mon Sep 17 00:00:00 2001 From: dm1sh Date: Thu, 20 Jul 2023 00:55:12 +0300 Subject: [PATCH 05/37] Fixed useFetch and useUser typing --- front/src/hooks/api/useUser.ts | 5 +++-- front/src/hooks/useFetch.ts | 27 +++++++++------------------ front/src/hooks/useStoryDimensions.ts | 6 ++++-- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/front/src/hooks/api/useUser.ts b/front/src/hooks/api/useUser.ts index 5531490..ca22c1a 100644 --- a/front/src/hooks/api/useUser.ts +++ b/front/src/hooks/api/useUser.ts @@ -1,8 +1,8 @@ import { initialUser } from '../../api/user' import { User } from '../../api/user/types' -import { UseFetchErrored, UseFetchSucced } from '../useFetch' +import { UseFetchReturn } from '../useFetch' -const useUser = (): UseFetchSucced | UseFetchErrored => ( +const useUser = (): UseFetchReturn => ( // useFetch( // composeUserUrl(getToken()), // 'GET', @@ -16,6 +16,7 @@ const useUser = (): UseFetchSucced | UseFetchErrored => ( data: initialUser, loading: false, error: null, + setData: () => {0} } ) diff --git a/front/src/hooks/useFetch.ts b/front/src/hooks/useFetch.ts index b401601..fdeda57 100644 --- a/front/src/hooks/useFetch.ts +++ b/front/src/hooks/useFetch.ts @@ -3,40 +3,31 @@ import { useEffect, useState } from 'react' import { SetState } from '../utils/types' import useSend from './useSend' -type UseFetchShared = { +type UseFetchShared = { loading: boolean, abort?: () => void, + setData: SetState } type UseFetchSucced = { error: null, data: T, -} & UseFetchShared +} & UseFetchShared -type UseFetchErrored = { +type UseFetchErrored = { error: string, data: undefined -} & UseFetchShared +} & UseFetchShared -const gotError = (res: UseFetchErrored | UseFetchSucced): res is UseFetchErrored => ( +const gotError = (res: UseFetchErrored | UseFetchSucced): res is UseFetchErrored => ( typeof res.error === 'string' ) -const fallbackError = (res: UseFetchSucced | UseFetchErrored) => ( +const fallbackError = (res: UseFetchSucced | UseFetchErrored) => ( gotError(res) ? res.error : res.data ) -type UseFetchReturn = ({ - error: null, - data: T -} | { - error: string, - data: undefined -}) & { - loading: boolean, - setData: SetState - abort?: (() => void) -} +type UseFetchReturn = UseFetchSucced | UseFetchErrored function useFetch( url: string, @@ -70,7 +61,7 @@ function useFetch( } } -export type { UseFetchErrored, UseFetchSucced } +export type { UseFetchReturn } export default useFetch diff --git a/front/src/hooks/useStoryDimensions.ts b/front/src/hooks/useStoryDimensions.ts index 7d69655..c0e2770 100644 --- a/front/src/hooks/useStoryDimensions.ts +++ b/front/src/hooks/useStoryDimensions.ts @@ -14,12 +14,14 @@ function useStoryDimensions(maxRatio = 16 / 9) { function handleResize() { setWindowDimensions(getWindowDimensions()); } - + window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); - const height = windowDimensions.height - 56 + const bottomBarHeight = 56 + + const height = windowDimensions.height - bottomBarHeight const ratio = Math.max(maxRatio, height / windowDimensions.width) From 58d1996ce3149dd625f292e8ec3386f89cd3c84f Mon Sep 17 00:00:00 2001 From: dm1sh Date: Thu, 20 Jul 2023 13:12:48 +0300 Subject: [PATCH 06/37] Added story index management via query on homepage Related to #27 --- front/src/hooks/index.ts | 1 + front/src/hooks/useStoryIndex.ts | 47 ++++++++++++++++++++++++++++++++ front/src/pages/HomePage.tsx | 28 ++++++++++++------- 3 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 front/src/hooks/useStoryIndex.ts diff --git a/front/src/hooks/index.ts b/front/src/hooks/index.ts index e1c5fa4..2e5d398 100644 --- a/front/src/hooks/index.ts +++ b/front/src/hooks/index.ts @@ -1,3 +1,4 @@ export { default as useStoryDimensions } from './useStoryDimensions' export { default as useSend } from './useSend' export { default as useFetch } from './useFetch' +export { default as UseStoryIndex } from './useStoryIndex' diff --git a/front/src/hooks/useStoryIndex.ts b/front/src/hooks/useStoryIndex.ts new file mode 100644 index 0000000..cff4630 --- /dev/null +++ b/front/src/hooks/useStoryIndex.ts @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react' +import { useSearchParams } from 'react-router-dom' +import { SetState } from '../utils/types' + +function useStoryIndex(annLength: number | undefined) { + const [index, setIndex] = useState(0) + + const [searchParams, setSearchParams] = useSearchParams() + + const withReset = (f: SetState) => (...args: Parameters>) => { + console.log('resetting index') + setIndex(0) + setSearchParams(prev => ({ ...prev, storyIndex: '0' }), { replace: true }) + f(...args) + } + + useEffect(() => { + setIndex(annLength ? + Number.parseInt(searchParams.get('storyIndex') || '0') : + 0) + // searchParams have actual query string at first render + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [annLength]) + + const increment = () => setIndex(prev => { + const newIndex = (prev + 1) % (annLength || 1) + setSearchParams(prev => ({ ...prev, storyIndex: newIndex.toString() }), { replace: true }) + + return newIndex + }) + + const decrement = () => setIndex(prev => { + const newIndex = prev > 0 ? (prev - 1) : 0 + setSearchParams(prev => ({ ...prev, storyIndex: newIndex.toString() }), { replace: true }) + + return newIndex + }) + + return { + n: index, + withReset, + increment, + decrement + } +} + +export default useStoryIndex diff --git a/front/src/pages/HomePage.tsx b/front/src/pages/HomePage.tsx index 9ac9435..2c4d36d 100644 --- a/front/src/pages/HomePage.tsx +++ b/front/src/pages/HomePage.tsx @@ -1,4 +1,4 @@ -import { CSSProperties, useEffect, useState } from 'react' +import { CSSProperties, useEffect, useMemo, useState } from 'react' import Stories from 'react-insta-stories' import { Story } from 'react-insta-stories/dist/interfaces' @@ -10,7 +10,8 @@ import { Announcement } from '../api/announcement/types' import { categoryGraphics } from '../assets/category' import puffSpinner from '../assets/puff.svg' -import { gotError } from '../hooks/useFetch' +import { UseFetchReturn, gotError } from '../hooks/useFetch' +import useStoryIndex from '../hooks/useStoryIndex' function generateStories(announcements: Announcement[]): Story[] { return announcements.map(announcement => { @@ -23,14 +24,14 @@ function generateStories(announcements: Announcement[]): Story[] { }) } -function fallbackGenerateStories(announcementsFetch: ReturnType) { - if (announcementsFetch.loading) +function fallbackGenerateStories(announcements: UseFetchReturn) { + if (announcements.loading) return fallbackStory() - if (gotError(announcementsFetch)) - return fallbackStory(announcementsFetch.error, true) + if (gotError(announcements)) + return fallbackStory(announcements.error, true) - const stories = generateStories(announcementsFetch.data) + const stories = generateStories(announcements.data) if (stories.length === 0) return fallbackStory('Здесь пока пусто') @@ -68,14 +69,21 @@ function HomePage() { const [filterShown, setFilterShown] = useState(false) const [filter, setFilter] = useState(defaultFilters) - const announcementsFetch = useAnnouncements(filter) - const stories = fallbackGenerateStories(announcementsFetch) + const announcements = useAnnouncements(filter) + + const stories = useMemo(() => fallbackGenerateStories(announcements), [announcements]) + + const index = useStoryIndex(announcements.data?.length) return (<> - +
Date: Thu, 20 Jul 2023 13:25:16 +0300 Subject: [PATCH 07/37] Fixed userPage story preview link to use actual filter Related to #27 --- front/src/components/StoriesPreview.tsx | 8 ++++---- front/src/utils/filters.ts | 8 +++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/front/src/components/StoriesPreview.tsx b/front/src/components/StoriesPreview.tsx index 24b8ffe..8871be0 100644 --- a/front/src/components/StoriesPreview.tsx +++ b/front/src/components/StoriesPreview.tsx @@ -1,12 +1,13 @@ import { Link } from 'react-router-dom' import { CSSProperties, useEffect, useMemo, useRef, useState } from 'react' +import { Button } from 'react-bootstrap' -import { UserCategory, userCategoriesInfos } from '../assets/userCategories' +import { UserCategory, composeUserCategoriesFilters, userCategoriesInfos } from '../assets/userCategories' import { Announcement } from '../api/announcement/types' import { categoryGraphics, categoryNames } from '../assets/category' +import { URLEncodeFilters } from '../utils/filters' import rightAngleIcon from '../assets/rightAngle.svg' -import { Button } from 'react-bootstrap' type StoriesPreviewProps = { announcements: Announcement[], @@ -113,8 +114,7 @@ function StoriesPreview({ announcements, category }: StoriesPreviewProps) { {useMemo(() => announcements.map((ann, i) => (
  • {ann.src?.endsWith('mp4') ? ( diff --git a/front/src/utils/filters.ts b/front/src/utils/filters.ts index 30a821f..ca4b6f0 100644 --- a/front/src/utils/filters.ts +++ b/front/src/utils/filters.ts @@ -10,7 +10,13 @@ const defaultFilters: FiltersType = { userId: undefined, category: undefined, me const URLEncodeFilters = (filters: FiltersType) => ( Object.fromEntries( filterNames.map( - fName => [fName, filters[fName]?.toString()] + fName => { + const v = filters[fName] + if (v) { + return [fName, encodeURIComponent(v)] + } + return [fName, undefined] + } ).filter((p): p is [string, string] => typeof p[1] !== 'undefined') ) ) From 9b4ef41030131e2b6eaa5c8c0001f164b4aa3514 Mon Sep 17 00:00:00 2001 From: dm1sh Date: Thu, 20 Jul 2023 14:58:05 +0300 Subject: [PATCH 08/37] Added query filters getting and setting on homepage fixes #27 --- front/src/hooks/index.ts | 3 ++- front/src/hooks/useFilters.ts | 47 +++++++++++++++++++++++++++++++++++ front/src/pages/HomePage.tsx | 9 +++---- front/src/utils/filters.ts | 19 +++++++++++++- front/src/utils/types.ts | 12 ++++++++- 5 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 front/src/hooks/useFilters.ts diff --git a/front/src/hooks/index.ts b/front/src/hooks/index.ts index 2e5d398..dd1b5ff 100644 --- a/front/src/hooks/index.ts +++ b/front/src/hooks/index.ts @@ -1,4 +1,5 @@ export { default as useStoryDimensions } from './useStoryDimensions' export { default as useSend } from './useSend' export { default as useFetch } from './useFetch' -export { default as UseStoryIndex } from './useStoryIndex' +export { default as useStoryIndex } from './useStoryIndex' +export { default as useFilters } from './useFilters' diff --git a/front/src/hooks/useFilters.ts b/front/src/hooks/useFilters.ts new file mode 100644 index 0000000..2394b13 --- /dev/null +++ b/front/src/hooks/useFilters.ts @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react' +import { useSearchParams } from 'react-router-dom' +import { FiltersType, URLDecoreFilters, URLEncodeFilters, defaultFilters } from '../utils/filters' +import { SetState } from '../utils/types' + +function useFilters(initialFilters: FiltersType = defaultFilters): [FiltersType, SetState] { + const [searchParams, setSearchParams] = useSearchParams() + + const [filters, setFilters] = useState(initialFilters) + + const appendFiltersSearchParams = (filters: FiltersType) => ( + setSearchParams(params => ({ + ...Object.fromEntries(params), + ...URLEncodeFilters(filters) + }), { replace: true }) + ) + + useEffect(() => { + const urlFilters = URLDecoreFilters(searchParams) + + setFilters(prev => ({ + ...prev, + ...urlFilters, + })) + + appendFiltersSearchParams({ + ...URLEncodeFilters(initialFilters), + ...URLEncodeFilters(urlFilters), + }) + // searchParams have actual query string at first render + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const withQuery = (f: SetState) => ( + (nextInit: (FiltersType | ((prev: FiltersType) => FiltersType))) => { + const newFilters = (typeof nextInit === 'function') ? nextInit(filters) : nextInit + + appendFiltersSearchParams(newFilters) + + f(nextInit) + } + ) + + return [filters, withQuery(setFilters)] +} + +export default useFilters diff --git a/front/src/pages/HomePage.tsx b/front/src/pages/HomePage.tsx index 2c4d36d..14afa98 100644 --- a/front/src/pages/HomePage.tsx +++ b/front/src/pages/HomePage.tsx @@ -3,15 +3,14 @@ import Stories from 'react-insta-stories' import { Story } from 'react-insta-stories/dist/interfaces' import { BottomNavBar, AnnouncementDetails, Filters } from '../components' -import { useStoryDimensions } from '../hooks' +import { useFilters, useStoryDimensions } from '../hooks' import { useAnnouncements } from '../hooks/api' -import { defaultFilters } from '../utils/filters' import { Announcement } from '../api/announcement/types' import { categoryGraphics } from '../assets/category' +import { UseFetchReturn, gotError } from '../hooks/useFetch' +import { useStoryIndex } from '../hooks' import puffSpinner from '../assets/puff.svg' -import { UseFetchReturn, gotError } from '../hooks/useFetch' -import useStoryIndex from '../hooks/useStoryIndex' function generateStories(announcements: Announcement[]): Story[] { return announcements.map(announcement => { @@ -67,8 +66,8 @@ function HomePage() { const { height, width } = useStoryDimensions(16 / 9) const [filterShown, setFilterShown] = useState(false) - const [filter, setFilter] = useState(defaultFilters) + const [filter, setFilter] = useFilters() const announcements = useAnnouncements(filter) diff --git a/front/src/utils/filters.ts b/front/src/utils/filters.ts index ca4b6f0..50039d6 100644 --- a/front/src/utils/filters.ts +++ b/front/src/utils/filters.ts @@ -1,4 +1,6 @@ import { Announcement } from '../api/announcement/types' +import { isCategory } from '../assets/category' +import { fallbackToUndefined, isInt } from './types' const filterNames = ['userId', 'category', 'metro', 'bookedBy'] as const type FilterNames = typeof filterNames[number] @@ -21,5 +23,20 @@ const URLEncodeFilters = (filters: FiltersType) => ( ) ) +const URLDecoreFilters = (params: URLSearchParams): FiltersType => { + const strFilters = Object.fromEntries( + filterNames.map( + fName => [fName, params.get(fName)] + ).filter((p): p is [string, string] => p[1] !== null) + ) + + return { + bookedBy: fallbackToUndefined(strFilters['bookedBy'], isInt), + category: fallbackToUndefined(strFilters['category'], isCategory), + metro: strFilters['metro'], + userId: fallbackToUndefined(strFilters['userId'], isInt) + } +} + export type { FilterNames, FiltersType } -export { defaultFilters, filterNames, URLEncodeFilters } +export { defaultFilters, filterNames, URLEncodeFilters, URLDecoreFilters } diff --git a/front/src/utils/types.ts b/front/src/utils/types.ts index 4ac3255..8f42cfa 100644 --- a/front/src/utils/types.ts +++ b/front/src/utils/types.ts @@ -60,8 +60,18 @@ const isString = (obj: unknown): obj is string => ( typeof obj === 'string' ) +const isInt = (obj: unknown): obj is number => ( + Number.isSafeInteger(obj) +) + +function fallbackToUndefined(obj: unknown, isT: ((obj: unknown) => obj is T)) { + if (!isT(obj)) return undefined + + return obj +} + type SetState = React.Dispatch> export type { SetState } -export { isRecord, isObject, isConst, isLiteralUnion, isArrayOf, isString } +export { isRecord, isObject, isConst, isLiteralUnion, isArrayOf, isString, isInt, fallbackToUndefined } From abe3e64883a2612fa56ca87d6a84e62df798381f Mon Sep 17 00:00:00 2001 From: dm1sh Date: Thu, 27 Jul 2023 12:00:22 +0300 Subject: [PATCH 09/37] Added initial loading setting Enabled pushing to history on filter setting --- front/src/hooks/useFetch.ts | 10 +++++----- front/src/hooks/useFilters.ts | 6 +++--- front/src/hooks/useSend.ts | 3 ++- front/src/utils/filters.ts | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/front/src/hooks/useFetch.ts b/front/src/hooks/useFetch.ts index fdeda57..327ba63 100644 --- a/front/src/hooks/useFetch.ts +++ b/front/src/hooks/useFetch.ts @@ -19,16 +19,16 @@ type UseFetchErrored = { data: undefined } & UseFetchShared -const gotError = (res: UseFetchErrored | UseFetchSucced): res is UseFetchErrored => ( +type UseFetchReturn = UseFetchSucced | UseFetchErrored + +const gotError = (res: UseFetchReturn): res is UseFetchErrored => ( typeof res.error === 'string' ) -const fallbackError = (res: UseFetchSucced | UseFetchErrored) => ( +const fallbackError = (res: UseFetchReturn) => ( gotError(res) ? res.error : res.data ) -type UseFetchReturn = UseFetchSucced | UseFetchErrored - function useFetch( url: string, method: RequestInit['method'], @@ -40,7 +40,7 @@ function useFetch( ): UseFetchReturn { const [data, setData] = useState(initialData) - const { doSend, loading, error } = useSend(url, method, needAuth, guardResponse, processResponse, params) + const { doSend, loading, error } = useSend(url, method, needAuth, guardResponse, processResponse, true, params) useEffect(() => { doSend().then( diff --git a/front/src/hooks/useFilters.ts b/front/src/hooks/useFilters.ts index 2394b13..e6238c6 100644 --- a/front/src/hooks/useFilters.ts +++ b/front/src/hooks/useFilters.ts @@ -8,11 +8,11 @@ function useFilters(initialFilters: FiltersType = defaultFilters): [FiltersType, const [filters, setFilters] = useState(initialFilters) - const appendFiltersSearchParams = (filters: FiltersType) => ( + const appendFiltersSearchParams = (filters: FiltersType, replace = false) => ( setSearchParams(params => ({ ...Object.fromEntries(params), ...URLEncodeFilters(filters) - }), { replace: true }) + }), { replace }) ) useEffect(() => { @@ -26,7 +26,7 @@ function useFilters(initialFilters: FiltersType = defaultFilters): [FiltersType, appendFiltersSearchParams({ ...URLEncodeFilters(initialFilters), ...URLEncodeFilters(urlFilters), - }) + }, true) // searchParams have actual query string at first render // eslint-disable-next-line react-hooks/exhaustive-deps }, []) diff --git a/front/src/hooks/useSend.ts b/front/src/hooks/useSend.ts index 6928512..48345bd 100644 --- a/front/src/hooks/useSend.ts +++ b/front/src/hooks/useSend.ts @@ -10,9 +10,10 @@ function useSend( needAuth: boolean, guardResponse: (data: unknown) => data is R, processResponse: (data: R) => T, + startWithLoading = false, defaultParams?: Omit ) { - const [loading, setLoading] = useState(false) + const [loading, setLoading] = useState(startWithLoading) const [error, setError] = useState(null) const navigate = useNavigate() diff --git a/front/src/utils/filters.ts b/front/src/utils/filters.ts index 50039d6..df05a09 100644 --- a/front/src/utils/filters.ts +++ b/front/src/utils/filters.ts @@ -15,7 +15,7 @@ const URLEncodeFilters = (filters: FiltersType) => ( fName => { const v = filters[fName] if (v) { - return [fName, encodeURIComponent(v)] + return [fName, v.toString()] } return [fName, undefined] } From e60d5d6732a42e900bd045d218d9971bc6949f88 Mon Sep 17 00:00:00 2001 From: dm1sh Date: Thu, 27 Jul 2023 12:53:27 +0300 Subject: [PATCH 10/37] Added user points indicator --- front/src/api/user/index.ts | 2 ++ front/src/api/user/types.ts | 2 ++ front/src/assets/userCategories.ts | 20 +------------------- front/src/components/Points.tsx | 24 ++++++++++++++++++++++++ front/src/components/index.ts | 1 + front/src/pages/UserPage.tsx | 3 ++- 6 files changed, 32 insertions(+), 20 deletions(-) create mode 100644 front/src/components/Points.tsx diff --git a/front/src/api/user/index.ts b/front/src/api/user/index.ts index f695a5f..34584cd 100644 --- a/front/src/api/user/index.ts +++ b/front/src/api/user/index.ts @@ -8,10 +8,12 @@ const initialUser: User = import.meta.env.DEV ? { // Temporary, until api is rea id: Math.random() * 100, name: faker.person.firstName() + ' ' + faker.person.lastName(), regDate: faker.date.anytime().getTime(), + points: Math.round(Math.random() * 1000), } : { id: -1, name: '', regDate: 0, + points: 0, } const composeUserURL = () => ( diff --git a/front/src/api/user/types.ts b/front/src/api/user/types.ts index 8c9ee7e..4f5d989 100644 --- a/front/src/api/user/types.ts +++ b/front/src/api/user/types.ts @@ -4,6 +4,7 @@ type User = { id: number, name: string, regDate: number, + points: number } const isUser = (obj: unknown): obj is User => ( @@ -11,6 +12,7 @@ const isUser = (obj: unknown): obj is User => ( 'id': 'number', 'name': 'string', 'regDate': 'number', + 'points': 'number', }) ) diff --git a/front/src/assets/userCategories.ts b/front/src/assets/userCategories.ts index c741c47..c51dcdd 100644 --- a/front/src/assets/userCategories.ts +++ b/front/src/assets/userCategories.ts @@ -1,26 +1,18 @@ import { Announcement } from '../api/announcement/types' import { FiltersType } from '../utils/filters' -const userCategories = ['givingOut', 'booked', 'history'] as const +const userCategories = ['givingOut'] as const type UserCategory = typeof userCategories[number] const UserCategoriesNames: Record = { givingOut: 'Раздача', - booked: 'Бронь', - history: 'История', } const userCategoriesInfos: Record string> = { givingOut: (ann: Announcement) => ( `Годен до ${new Date(ann.bestBy).toLocaleDateString('ru')}` ), - booked: (ann: Announcement) => ( - `Бронь ещё ${(ann as Announcement & { bookedBy: number[] }).bookedBy.length} чел.` - ), - history: (ann: Announcement) => ( - `Забрал ${new Date((ann as Announcement & { taken: number }).taken).toLocaleDateString('ru')}` - ), } const composeUserCategoriesFilters: Record FiltersType> = { @@ -29,16 +21,6 @@ const composeUserCategoriesFilters: Record FiltersType> = { return ({ userId }) }, - booked: () => { - const userId = -1 - - return ({ bookedBy: userId }) - }, - history: () => { - const userId = -1 - - return ({ userId, status: 'taken' }) - } } export type { UserCategory } diff --git a/front/src/components/Points.tsx b/front/src/components/Points.tsx new file mode 100644 index 0000000..6ff6a6a --- /dev/null +++ b/front/src/components/Points.tsx @@ -0,0 +1,24 @@ +import { CSSProperties } from 'react' + +type PointsProps = { + points?: number +} + +const styles = { + container: { + paddingBottom: 8, + } as CSSProperties, + points: { + float: 'right', + } as CSSProperties, +} + +function Points({ points }: PointsProps) { + return ( +
    +
    Набрано очков: {points}
    +
    + ) +} + +export default Points diff --git a/front/src/components/index.ts b/front/src/components/index.ts index 6562e70..6d26007 100644 --- a/front/src/components/index.ts +++ b/front/src/components/index.ts @@ -10,3 +10,4 @@ export { default as AuthForm } from './AuthForm' export { default as BackHeader } from './BackHeader' export { default as CategoryPreview } from './CategoryPreview' export { default as StoriesPreview } from './StoriesPreview' +export { default as Points } from './Points' diff --git a/front/src/pages/UserPage.tsx b/front/src/pages/UserPage.tsx index a35a1be..8d8b310 100644 --- a/front/src/pages/UserPage.tsx +++ b/front/src/pages/UserPage.tsx @@ -3,7 +3,7 @@ import { Container } from 'react-bootstrap' import BackHeader from '../components/BackHeader' import { useUser } from '../hooks/api' import { userCategories } from '../assets/userCategories' -import { CategoryPreview } from '../components' +import { CategoryPreview, Points } from '../components' import { gotError } from '../hooks/useFetch' function UserPage() { @@ -16,6 +16,7 @@ function UserPage() { user.error : `${user.data.name}, с нами с ${new Date(user.data.regDate).toLocaleDateString('ru')}` } /> + {userCategories.map(cat => ( ))} From 40c5f08dfe4f9c90c663de4724bf286b8d6fa17e Mon Sep 17 00:00:00 2001 From: dm1sh Date: Thu, 27 Jul 2023 14:16:12 +0300 Subject: [PATCH 11/37] Added points icon --- front/src/assets/handStars.svg | 7 +++++++ front/src/components/Points.tsx | 11 ++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 front/src/assets/handStars.svg diff --git a/front/src/assets/handStars.svg b/front/src/assets/handStars.svg new file mode 100644 index 0000000..e14b406 --- /dev/null +++ b/front/src/assets/handStars.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/front/src/components/Points.tsx b/front/src/components/Points.tsx index 6ff6a6a..fee87ff 100644 --- a/front/src/components/Points.tsx +++ b/front/src/components/Points.tsx @@ -1,5 +1,7 @@ import { CSSProperties } from 'react' +import handStarsIcon from '../assets/handStars.svg' + type PointsProps = { points?: number } @@ -11,12 +13,19 @@ const styles = { points: { float: 'right', } as CSSProperties, + icon: { + height: 24, + paddingBottom: 5, + } as CSSProperties, } function Points({ points }: PointsProps) { return (
    -
    Набрано очков: {points}
    +
    + Набрано очков: + Hand giving stars icon {points} +
    ) } From 8220b43e9b232e66db782ac235a8d57a839f36b6 Mon Sep 17 00:00:00 2001 From: dm1sh Date: Thu, 27 Jul 2023 16:50:26 +0300 Subject: [PATCH 12/37] Fixed possibly nullable useFetch or useSend result data --- front/src/hooks/useFetch.ts | 2 +- front/src/hooks/useSend.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/front/src/hooks/useFetch.ts b/front/src/hooks/useFetch.ts index 327ba63..e20a86e 100644 --- a/front/src/hooks/useFetch.ts +++ b/front/src/hooks/useFetch.ts @@ -29,7 +29,7 @@ const fallbackError = (res: UseFetchReturn) => ( gotError(res) ? res.error : res.data ) -function useFetch( +function useFetch>( url: string, method: RequestInit['method'], needAuth: boolean, diff --git a/front/src/hooks/useSend.ts b/front/src/hooks/useSend.ts index 48345bd..12e1839 100644 --- a/front/src/hooks/useSend.ts +++ b/front/src/hooks/useSend.ts @@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom' import { getToken } from '../utils/auth' import { handleHTTPErrors, isAborted } from '../utils' -function useSend( +function useSend>( url: string, method: RequestInit['method'], needAuth: boolean, From 2a229c96ba29abdc6c8559d1d3a8f594a93677a4 Mon Sep 17 00:00:00 2001 From: dm1sh Date: Thu, 27 Jul 2023 16:55:07 +0300 Subject: [PATCH 13/37] Separated useSendWithButton hook from useAddAnnouncement --- front/src/hooks/api/useAddAnnouncement.ts | 29 +++++++---------------- front/src/hooks/index.ts | 1 + front/src/hooks/useSendWithButton.ts | 25 +++++++++++++++++++ 3 files changed, 34 insertions(+), 21 deletions(-) create mode 100644 front/src/hooks/useSendWithButton.ts diff --git a/front/src/hooks/api/useAddAnnouncement.ts b/front/src/hooks/api/useAddAnnouncement.ts index 5c60488..ab46a30 100644 --- a/front/src/hooks/api/useAddAnnouncement.ts +++ b/front/src/hooks/api/useAddAnnouncement.ts @@ -1,31 +1,18 @@ -import { useCallback } from 'react' - -import { useSend } from '..' +import { useSendWithButton } from '..' import { composePutAnnouncementURL, processPutAnnouncement } from '../../api/putAnnouncement' import { isPutAnnouncementResponse } from '../../api/putAnnouncement/types' -import useSendButtonCaption from '../useSendButtonCaption' -const useAddAnnouncement = () => { - const { doSend, loading, error } = useSend( +const useAddAnnouncement = () => ( + useSendWithButton( + 'Опубликовать', + 'Опубликовано', + true, composePutAnnouncementURL(), 'PUT', true, isPutAnnouncementResponse, - processPutAnnouncement, + processPutAnnouncement ) - - const { update, ...button } = useSendButtonCaption('Опубликовать', loading, error, 'Опубликовано') - - const doSendWithButton = useCallback(async (formData: FormData) => { - const data = await doSend({}, { - body: formData - }) - update(data) - - return data - }, [doSend, update]) - - return { doSend: doSendWithButton, button } -} +) export default useAddAnnouncement diff --git a/front/src/hooks/index.ts b/front/src/hooks/index.ts index dd1b5ff..5a757f7 100644 --- a/front/src/hooks/index.ts +++ b/front/src/hooks/index.ts @@ -3,3 +3,4 @@ export { default as useSend } from './useSend' export { default as useFetch } from './useFetch' export { default as useStoryIndex } from './useStoryIndex' export { default as useFilters } from './useFilters' +export { default as useSendWithButton } from './useSendWithButton' diff --git a/front/src/hooks/useSendWithButton.ts b/front/src/hooks/useSendWithButton.ts new file mode 100644 index 0000000..5115422 --- /dev/null +++ b/front/src/hooks/useSendWithButton.ts @@ -0,0 +1,25 @@ +import { useCallback } from 'react' +import { useSend } from '.' +import useSendButtonCaption from './useSendButtonCaption' + +function useSendWithButton>( + initial: string, + result: string, + singular?: boolean, + ...useSendArgs: Parameters> +) { + const { doSend, loading, error } = useSend(...useSendArgs) + + const { update, ...button } = useSendButtonCaption(initial, loading, error, result, singular) + + const doSendWithButton = useCallback(async (params: Parameters[1]) => { + const data = await doSend({}, params) + + update(data) + + }, [doSend, update]) + + return { doSend: doSendWithButton, button } +} + +export default useSendWithButton From 3bb6809454ccc8d9abf76e5ecebe9a95ca653731 Mon Sep 17 00:00:00 2001 From: dm1sh Date: Thu, 27 Jul 2023 16:56:00 +0300 Subject: [PATCH 14/37] Moved useBook to useSend API --- front/src/api/book/index.ts | 12 +++ front/src/api/book/types.ts | 17 +++++ front/src/components/AnnouncementDetails.tsx | 10 +-- front/src/hooks/api/useBook.ts | 79 ++++++-------------- 4 files changed, 57 insertions(+), 61 deletions(-) create mode 100644 front/src/api/book/index.ts create mode 100644 front/src/api/book/types.ts diff --git a/front/src/api/book/index.ts b/front/src/api/book/index.ts new file mode 100644 index 0000000..22fd424 --- /dev/null +++ b/front/src/api/book/index.ts @@ -0,0 +1,12 @@ +import { API_URL } from '../../config' +import { Book, BookResponse } from './types' + +const composeBookURL = () => ( + API_URL + '/book?' +) + +const processBook = (data: BookResponse): Book => { + return data.Success +} + +export { composeBookURL, processBook } diff --git a/front/src/api/book/types.ts b/front/src/api/book/types.ts new file mode 100644 index 0000000..8e7c134 --- /dev/null +++ b/front/src/api/book/types.ts @@ -0,0 +1,17 @@ +import { isObject } from '../../utils/types' + +type BookResponse = { + Success: boolean +} + +const isBookResponse = (obj: unknown): obj is BookResponse => ( + isObject(obj, { + 'Success': 'boolean' + }) +) + +type Book = boolean + +export type { BookResponse, Book } + +export { isBookResponse } diff --git a/front/src/components/AnnouncementDetails.tsx b/front/src/components/AnnouncementDetails.tsx index 352b138..b604909 100644 --- a/front/src/components/AnnouncementDetails.tsx +++ b/front/src/components/AnnouncementDetails.tsx @@ -21,8 +21,10 @@ const styles = { } as CSSProperties, } -function AnnouncementDetails({ close, announcement: { id, name, category, bestBy, description, lat, lng, address, metro } }: AnnouncementDetailsProps) { - const { handleBook, status: bookStatus } = useBook(id) +function AnnouncementDetails({ close, announcement: { + id, name, category, bestBy, description, lat, lng, address, metro +} }: AnnouncementDetailsProps) { + const { handleBook, bookButton } = useBook() return (
    - +
    diff --git a/front/src/hooks/api/useBook.ts b/front/src/hooks/api/useBook.ts index 71fe6ed..5a1310b 100644 --- a/front/src/hooks/api/useBook.ts +++ b/front/src/hooks/api/useBook.ts @@ -1,10 +1,7 @@ -import { useState } from 'react' -import { useNavigate } from 'react-router-dom' - -import { getToken } from '../../utils/auth' -import { API_URL } from '../../config' +import { useCallback } from 'react' import { isObject } from '../../utils/types' -import { handleHTTPErrors } from '../../utils' +import { useSendWithButton } from '..' +import { composeBookURL, processBook } from '../../api/book' type BookResponse = { Success: boolean @@ -16,59 +13,29 @@ const isBookResponse = (obj: unknown): obj is BookResponse => ( }) ) -type BookStatus = '' | 'Загрузка...' | 'Забронировано' | 'Ошибка бронирования' +function useBook() { + const { doSend, button } = useSendWithButton('Забронировать', + 'Забронировано', + true, + composeBookURL(), + 'POST', + true, + isBookResponse, + processBook + ) -function useBook(id: number) { - const navigate = useNavigate() - - const [status, setStatus] = useState('') - - const handleBook = async () => { - const token = getToken() - - if (token) { - setStatus('Загрузка...') - - try { - - const res = await fetch(API_URL + '/book', { - method: 'POST', - body: JSON.stringify({ - id: id - }), - headers: { - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json' - } - }) - - handleHTTPErrors(res) - - const data: unknown = await res.json() - - if (!isBookResponse(data)) { - throw new Error('Malformed server response') - } - - if (data.Success === true) { - setStatus('Забронировано') - } else { - throw new Error('Server refused to book') - } + const handleBook = useCallback((id: number) => { + void doSend({ + body: JSON.stringify({ + id + }), + headers: { + 'Content-Type': 'application/json' } - catch (err) { - setStatus('Ошибка бронирования') + }) + }, [doSend]) - if (import.meta.env.DEV) { - console.log(err) - } - } - } else { - return navigate('/login') - } - } - - return { handleBook, status } + return { handleBook, bookButton: button } } export default useBook From 9688f56c43b26e0d31ab0089756d198eca7417c3 Mon Sep 17 00:00:00 2001 From: dm1sh Date: Thu, 27 Jul 2023 17:54:06 +0300 Subject: [PATCH 15/37] Added useId hook for id retrival from token --- front/package-lock.json | 6 +++ front/package.json | 1 + front/src/hooks/index.ts | 1 + front/src/hooks/useId.ts | 16 ++++++++ front/src/pages/LoginPage.tsx | 6 ++- front/src/utils/auth.ts | 75 ++++++++++++++++++++++++++++++++--- 6 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 front/src/hooks/useId.ts diff --git a/front/package-lock.json b/front/package-lock.json index 52f0739..2b86f66 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@types/leaflet": "^1.9.3", "bootstrap": "^5.3.0", + "jwt-decode": "^3.1.2", "leaflet": "^1.9.4", "react": "^18.2.0", "react-bootstrap": "^2.8.0", @@ -2378,6 +2379,11 @@ "node": ">=6" } }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "node_modules/leaflet": { "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", diff --git a/front/package.json b/front/package.json index e848a0c..163e525 100644 --- a/front/package.json +++ b/front/package.json @@ -14,6 +14,7 @@ "dependencies": { "@types/leaflet": "^1.9.3", "bootstrap": "^5.3.0", + "jwt-decode": "^3.1.2", "leaflet": "^1.9.4", "react": "^18.2.0", "react-bootstrap": "^2.8.0", diff --git a/front/src/hooks/index.ts b/front/src/hooks/index.ts index 5a757f7..4d42417 100644 --- a/front/src/hooks/index.ts +++ b/front/src/hooks/index.ts @@ -4,3 +4,4 @@ export { default as useFetch } from './useFetch' export { default as useStoryIndex } from './useStoryIndex' export { default as useFilters } from './useFilters' export { default as useSendWithButton } from './useSendWithButton' +export { default as useId } from './useId' diff --git a/front/src/hooks/useId.ts b/front/src/hooks/useId.ts new file mode 100644 index 0000000..f3f2e0a --- /dev/null +++ b/front/src/hooks/useId.ts @@ -0,0 +1,16 @@ +import { useNavigate } from 'react-router-dom' +import { getId } from '../utils/auth' + +function useId() { + const navigate = useNavigate() + + const id = getId() + + if (id < 0) { + navigate('/login') + } + + return id +} + +export default useId diff --git a/front/src/pages/LoginPage.tsx b/front/src/pages/LoginPage.tsx index fd1a8cb..3ef08a5 100644 --- a/front/src/pages/LoginPage.tsx +++ b/front/src/pages/LoginPage.tsx @@ -24,7 +24,11 @@ function LoginPage() { password: formData.get('password') as string } - const token = import.meta.env.PROD ? await doAuth(data, newAccount) : 'a' + const token = import.meta.env.PROD ? ( + await doAuth(data, newAccount) + ) : ( + 'eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjUifQ.1S46AuB9E4JN9yLkqs30yl3sLlGLbgbrOCNKXiNK8IM' + ) if (token) { setToken(token) diff --git a/front/src/utils/auth.ts b/front/src/utils/auth.ts index a380ee4..0f37b67 100644 --- a/front/src/utils/auth.ts +++ b/front/src/utils/auth.ts @@ -1,13 +1,76 @@ -const getToken = () => { - const token = localStorage.getItem('Token') +import jwt_decode, { JwtPayload } from 'jwt-decode' - /* check expirity */ +import { isInt, isObject } from './types' - return token +const TOKEN_KEY = 'Token' + +function getToken() { + const token = localStorage.getItem(TOKEN_KEY) + + if (token === null) { + return null + } + + const payload = jwt_decode(token) + + // Checks only expiration, not validity + if ( + payload.exp && + (Date.now() >= payload.exp * 1000) + ) { + return null + } + + return token } function setToken(token: string) { - localStorage.setItem('Token', token) + localStorage.setItem(TOKEN_KEY, token) } -export { getToken, setToken } +function clearToken() { + localStorage.removeItem(TOKEN_KEY) +} + +type TokenPayload = { + id: string +} + +const isTokenPayload = (data: unknown): data is TokenPayload => isObject(data, { + 'id': 'string' +}) + +function getId() { + try { + const token = getToken() + + if (token === null) { + return -1 + } + + const payload = jwt_decode(token) + + if (!isTokenPayload(payload)) { + throw new Error('Malformed token payload') + } + + const id = Number.parseInt(payload.id) + + if (!isInt(id) || id < 0) { + throw new Error(`Not valid id: ${id}`) + } + + return id + } + catch (err) { + if (import.meta.env.DEV) { + console.error(err) + } + + clearToken() + + return -1 + } +} + +export { getToken, setToken, getId } From d93b2e131c6257f2f084dbfcec12aabcf1da19d7 Mon Sep 17 00:00:00 2001 From: dm1sh Date: Thu, 27 Jul 2023 18:43:37 +0300 Subject: [PATCH 16/37] Added announcement removal for published by user --- front/src/api/removeAnnouncement/index.ts | 12 ++++++++ front/src/api/removeAnnouncement/types.ts | 17 +++++++++++ front/src/components/AnnouncementDetails.tsx | 21 ++++++++++--- front/src/hooks/api/index.ts | 1 + front/src/hooks/api/useRemoveAnnouncement.ts | 31 ++++++++++++++++++++ 5 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 front/src/api/removeAnnouncement/index.ts create mode 100644 front/src/api/removeAnnouncement/types.ts create mode 100644 front/src/hooks/api/useRemoveAnnouncement.ts diff --git a/front/src/api/removeAnnouncement/index.ts b/front/src/api/removeAnnouncement/index.ts new file mode 100644 index 0000000..fa88c16 --- /dev/null +++ b/front/src/api/removeAnnouncement/index.ts @@ -0,0 +1,12 @@ +import { API_URL } from '../../config' +import { RemoveAnnouncement, RemoveAnnouncementResponse } from './types' + +const composeRemoveAnnouncementURL = () => ( + API_URL + '/announcement?' +) + +const processRemoveAnnouncement = (data: RemoveAnnouncementResponse): RemoveAnnouncement => { + return data.Answer +} + +export { composeRemoveAnnouncementURL, processRemoveAnnouncement } diff --git a/front/src/api/removeAnnouncement/types.ts b/front/src/api/removeAnnouncement/types.ts new file mode 100644 index 0000000..0c5bb2b --- /dev/null +++ b/front/src/api/removeAnnouncement/types.ts @@ -0,0 +1,17 @@ +import { isObject } from '../../utils/types' + +type RemoveAnnouncementResponse = { + Answer: boolean +} + +const isRemoveAnnouncementResponse = (obj: unknown): obj is RemoveAnnouncementResponse => ( + isObject(obj, { + 'Answer': 'boolean' + }) +) + +type RemoveAnnouncement = boolean + +export type { RemoveAnnouncementResponse, RemoveAnnouncement } + +export { isRemoveAnnouncementResponse } diff --git a/front/src/components/AnnouncementDetails.tsx b/front/src/components/AnnouncementDetails.tsx index b604909..4a20086 100644 --- a/front/src/components/AnnouncementDetails.tsx +++ b/front/src/components/AnnouncementDetails.tsx @@ -3,10 +3,11 @@ import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet' import LineDot from './LineDot' import { categoryNames } from '../assets/category' -import { useBook } from '../hooks/api' +import { useBook, useRemoveAnnouncement } from '../hooks/api' import { Announcement } from '../api/announcement/types' import { iconItem } from '../utils/markerIcons' import { CSSProperties } from 'react' +import { useId } from '../hooks' type AnnouncementDetailsProps = { close: () => void, @@ -19,12 +20,19 @@ const styles = { alignItems: 'center', justifyContent: 'center', } as CSSProperties, + map: { + width: '100%', + minHeight: 300, + } as CSSProperties, } function AnnouncementDetails({ close, announcement: { - id, name, category, bestBy, description, lat, lng, address, metro + id, name, category, bestBy, description, lat, lng, address, metro, bookedBy, userId } }: AnnouncementDetailsProps) { const { handleBook, bookButton } = useBook() + const { handleRemove, removeButton } = useRemoveAnnouncement(close) + + const myId = useId() return (
    {description}

    - + -
    diff --git a/front/src/hooks/api/index.ts b/front/src/hooks/api/index.ts index ce87039..1d3d229 100644 --- a/front/src/hooks/api/index.ts +++ b/front/src/hooks/api/index.ts @@ -5,3 +5,4 @@ export { default as useTrashboxes } from './useTrashboxes' export { default as useAddAnnouncement } from './useAddAnnouncement' export { default as useOsmAddresses } from './useOsmAddress' export { default as useUser } from './useUser' +export { default as useRemoveAnnouncement } from './useRemoveAnnouncement' diff --git a/front/src/hooks/api/useRemoveAnnouncement.ts b/front/src/hooks/api/useRemoveAnnouncement.ts new file mode 100644 index 0000000..f24ca00 --- /dev/null +++ b/front/src/hooks/api/useRemoveAnnouncement.ts @@ -0,0 +1,31 @@ +import { useCallback } from 'react' +import { useSendWithButton } from '..' +import { composeRemoveAnnouncementURL, processRemoveAnnouncement } from '../../api/removeAnnouncement' +import { isRemoveAnnouncementResponse } from '../../api/removeAnnouncement/types' + +const useRemoveAnnouncement = (close: () => void) => { + const { doSend, button } = useSendWithButton( + 'Удалить', + 'Удалено', + true, + composeRemoveAnnouncementURL(), + 'DELETE', + true, + isRemoveAnnouncementResponse, + processRemoveAnnouncement + ) + + const doSendWithClose = useCallback(async (id: number) => { + await doSend({ + body: JSON.stringify({ + id + }) + }) + + close() + }, [doSend, close]) + + return { handleRemove: doSendWithClose, removeButton: button } +} + +export default useRemoveAnnouncement From 50c2b0a61588a5aa5626ec43ab0123a6733ab373 Mon Sep 17 00:00:00 2001 From: dm1sh Date: Thu, 27 Jul 2023 20:07:25 +0300 Subject: [PATCH 17/37] Added sign out button to user page --- front/src/assets/signOut.svg | 4 ++++ front/src/components/BackHeader.tsx | 6 ++++-- front/src/components/Points.tsx | 13 ++++++++++--- front/src/components/SignOut.tsx | 22 ++++++++++++++++++++++ front/src/components/index.ts | 1 + front/src/pages/UserPage.tsx | 16 ++++++++++------ front/src/utils/auth.ts | 2 +- 7 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 front/src/assets/signOut.svg create mode 100644 front/src/components/SignOut.tsx diff --git a/front/src/assets/signOut.svg b/front/src/assets/signOut.svg new file mode 100644 index 0000000..fdaeff6 --- /dev/null +++ b/front/src/assets/signOut.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/front/src/components/BackHeader.tsx b/front/src/components/BackHeader.tsx index e8ba89f..dca8c0b 100644 --- a/front/src/components/BackHeader.tsx +++ b/front/src/components/BackHeader.tsx @@ -2,22 +2,24 @@ import { Link } from 'react-router-dom' import { Navbar } from 'react-bootstrap' import BackButton from '../assets/backArrow.svg' +import { PropsWithChildren } from 'react' type BackHeaderProps = { text: string } -function BackHeader({ text }: BackHeaderProps) { +function BackHeader({ text, children }: PropsWithChildren) { return ( - Go back + Назад

    {text}

    + {children}
    ) } diff --git a/front/src/components/Points.tsx b/front/src/components/Points.tsx index fee87ff..184d876 100644 --- a/front/src/components/Points.tsx +++ b/front/src/components/Points.tsx @@ -3,7 +3,7 @@ import { CSSProperties } from 'react' import handStarsIcon from '../assets/handStars.svg' type PointsProps = { - points?: number + points: number } const styles = { @@ -16,6 +16,7 @@ const styles = { icon: { height: 24, paddingBottom: 5, + marginRight: 5, } as CSSProperties, } @@ -23,8 +24,14 @@ function Points({ points }: PointsProps) { return (
    - Набрано очков: - Hand giving stars icon {points} + Набрано очков: + + Иконка руки, дающей звёзды + {points} +
    ) diff --git a/front/src/components/SignOut.tsx b/front/src/components/SignOut.tsx new file mode 100644 index 0000000..9e01c3c --- /dev/null +++ b/front/src/components/SignOut.tsx @@ -0,0 +1,22 @@ +import { Navbar } from 'react-bootstrap' +import { Link } from 'react-router-dom' +import { CSSProperties } from 'react' + +import { clearToken } from '../utils/auth' + +import signOutIcon from '../assets/signOut.svg' + +const styles = { + rightIcon: { + marginLeft: '1rem', + marginRight: 0 + } as CSSProperties, +} + +const SignOut = () => ( + + Выйти + +) + +export default SignOut diff --git a/front/src/components/index.ts b/front/src/components/index.ts index 6d26007..4ec2ca0 100644 --- a/front/src/components/index.ts +++ b/front/src/components/index.ts @@ -11,3 +11,4 @@ export { default as BackHeader } from './BackHeader' export { default as CategoryPreview } from './CategoryPreview' export { default as StoriesPreview } from './StoriesPreview' export { default as Points } from './Points' +export { default as SignOut } from './SignOut' diff --git a/front/src/pages/UserPage.tsx b/front/src/pages/UserPage.tsx index 8d8b310..7ba9603 100644 --- a/front/src/pages/UserPage.tsx +++ b/front/src/pages/UserPage.tsx @@ -1,9 +1,8 @@ import { Container } from 'react-bootstrap' -import BackHeader from '../components/BackHeader' import { useUser } from '../hooks/api' import { userCategories } from '../assets/userCategories' -import { CategoryPreview, Points } from '../components' +import { BackHeader, CategoryPreview, Points, SignOut } from '../components' import { gotError } from '../hooks/useFetch' function UserPage() { @@ -12,11 +11,16 @@ function UserPage() { return ( - + ) + }> + + + + {userCategories.map(cat => ( ))} diff --git a/front/src/utils/auth.ts b/front/src/utils/auth.ts index 0f37b67..65fac6e 100644 --- a/front/src/utils/auth.ts +++ b/front/src/utils/auth.ts @@ -73,4 +73,4 @@ function getId() { } } -export { getToken, setToken, getId } +export { getToken, setToken, clearToken, getId } From 0e5aeae491fe64f8ead64ce61f0b0beee03369ec Mon Sep 17 00:00:00 2001 From: dm1sh Date: Thu, 27 Jul 2023 20:09:03 +0300 Subject: [PATCH 18/37] Fixed doSend arguments --- front/src/hooks/api/useAddAnnouncement.ts | 14 +++++++++++--- front/src/hooks/api/useBook.ts | 2 +- front/src/hooks/api/useRemoveAnnouncement.ts | 2 +- front/src/hooks/useSendWithButton.ts | 5 ++--- front/src/pages/AddPage.tsx | 6 +++--- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/front/src/hooks/api/useAddAnnouncement.ts b/front/src/hooks/api/useAddAnnouncement.ts index ab46a30..c4ea9d9 100644 --- a/front/src/hooks/api/useAddAnnouncement.ts +++ b/front/src/hooks/api/useAddAnnouncement.ts @@ -2,8 +2,8 @@ import { useSendWithButton } from '..' import { composePutAnnouncementURL, processPutAnnouncement } from '../../api/putAnnouncement' import { isPutAnnouncementResponse } from '../../api/putAnnouncement/types' -const useAddAnnouncement = () => ( - useSendWithButton( +function useAddAnnouncement() { + const { doSend, button } = useSendWithButton( 'Опубликовать', 'Опубликовано', true, @@ -13,6 +13,14 @@ const useAddAnnouncement = () => ( isPutAnnouncementResponse, processPutAnnouncement ) -) + + function handleAdd(formData: FormData) { + void doSend({}, { + body: formData + }) + } + + return { handleAdd, addButton: button } +} export default useAddAnnouncement diff --git a/front/src/hooks/api/useBook.ts b/front/src/hooks/api/useBook.ts index 5a1310b..e427e9b 100644 --- a/front/src/hooks/api/useBook.ts +++ b/front/src/hooks/api/useBook.ts @@ -25,7 +25,7 @@ function useBook() { ) const handleBook = useCallback((id: number) => { - void doSend({ + void doSend({}, { body: JSON.stringify({ id }), diff --git a/front/src/hooks/api/useRemoveAnnouncement.ts b/front/src/hooks/api/useRemoveAnnouncement.ts index f24ca00..ca8271f 100644 --- a/front/src/hooks/api/useRemoveAnnouncement.ts +++ b/front/src/hooks/api/useRemoveAnnouncement.ts @@ -16,7 +16,7 @@ const useRemoveAnnouncement = (close: () => void) => { ) const doSendWithClose = useCallback(async (id: number) => { - await doSend({ + await doSend({}, { body: JSON.stringify({ id }) diff --git a/front/src/hooks/useSendWithButton.ts b/front/src/hooks/useSendWithButton.ts index 5115422..300356d 100644 --- a/front/src/hooks/useSendWithButton.ts +++ b/front/src/hooks/useSendWithButton.ts @@ -12,11 +12,10 @@ function useSendWithButton>( const { update, ...button } = useSendButtonCaption(initial, loading, error, result, singular) - const doSendWithButton = useCallback(async (params: Parameters[1]) => { - const data = await doSend({}, params) + const doSendWithButton = useCallback(async (...args: Parameters) => { + const data = await doSend(...args) update(data) - }, [doSend, update]) return { doSend: doSendWithButton, button } diff --git a/front/src/pages/AddPage.tsx b/front/src/pages/AddPage.tsx index ac4c292..eaa8195 100644 --- a/front/src/pages/AddPage.tsx +++ b/front/src/pages/AddPage.tsx @@ -31,7 +31,7 @@ function AddPage() { const address = useOsmAddresses(addressPosition) - const { doSend, button } = useAddAnnouncement() + const { handleAdd, addButton } = useAddAnnouncement() const handleSubmit: FormEventHandler = (event) => { event.preventDefault() @@ -44,7 +44,7 @@ function AddPage() { formData.append('address', address.data || '') // if address.error formData.set('bestBy', new Date((formData.get('bestBy') as number | null) || 0).getTime().toString()) - void doSend(formData) + handleAdd(formData) } return ( @@ -176,7 +176,7 @@ function AddPage() { )} - + +
  • ) } 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({ index: -1, category: '' }) + + const { handleDispose, disposeButton } = useDispose(closeRefresh) + + return ( + <> + +
    + {gotResponse(trashboxes) + ? ( + gotError(trashboxes) ? ( +

    {trashboxes.error}

    + ) : ( + + + + + ) + ) : ( +
    +

    Загрузка...

    +
    + ) + + } +
    + +
    + + + ) +} + +type RatingProps = { + userId: number, + className: string | undefined, +} + +function Rating({ userId, className }: RatingProps) { + const rating = useUserRating(userId) + + const [myRate, setMyRate] = useState(0) + + const { doSendRate } = useSendRate() + + async function sendMyRate() { + const res = await doSendRate(myRate) + + if (res) { + rating.refetch() + } + } + + return ( +

    + Рейтинг пользователя:{' '} + {gotResponse(rating) ? ( + gotError(rating) ? ( + {rating.error} + ) : ( + setMyRate(0)} + > + {...Array(5).fill(5).map( + (_, i) => + setMyRate(i + 1)} + sendMyRate={() => void sendMyRate()} + /> + )} + + ) + ) : ( + Загрузка... + ) + } +

    + + ) +} + +export default Rating diff --git a/front/src/components/index.ts b/front/src/components/index.ts index 57f2928..b124f0c 100644 --- a/front/src/components/index.ts +++ b/front/src/components/index.ts @@ -14,3 +14,4 @@ export { default as Points } from './Points' export { default as SignOut } from './SignOut' export { default as Poetry } from './Poetry' export { default as SelectDisposalTrashbox } from './SelectDisposalTrashbox' +export { default as Rating } from './Rating' diff --git a/front/src/hooks/api/index.ts b/front/src/hooks/api/index.ts index 93062d8..7e2d8f9 100644 --- a/front/src/hooks/api/index.ts +++ b/front/src/hooks/api/index.ts @@ -9,3 +9,5 @@ export { default as useSignIn } from './useSignIn' export { default as useSignUp } from './useSignUp' export { default as usePoetry } from './usePoetry' export { default as useDispose } from './useDispose' +export { default as useSendRate } from './useSendRate' +export { default as useUserRating } from './useUserRating' diff --git a/front/src/hooks/api/useSendRate.ts b/front/src/hooks/api/useSendRate.ts new file mode 100644 index 0000000..d3cfc03 --- /dev/null +++ b/front/src/hooks/api/useSendRate.ts @@ -0,0 +1,33 @@ +import { useSend } from '..' +import { composeSendRateURL, processSendRate } from '../../api/sendRate' +import { isSendRateResponse } from '../../api/sendRate/types' + +function useSendRate() { + const { doSend, ...rest } = useSend( + composeSendRateURL(), + 'POST', + true, + isSendRateResponse, + processSendRate, + ) + + const doSendRate = (rate: number) => ( + doSend({}, { + body: JSON.stringify({ + rate, + }), + headers: { + 'Content-Type': 'application/json', + }, + + }) + ) + + + return { + doSendRate, + ...rest, + } +} + +export default useSendRate diff --git a/front/src/hooks/api/useUserRating.ts b/front/src/hooks/api/useUserRating.ts new file mode 100644 index 0000000..b14a5bf --- /dev/null +++ b/front/src/hooks/api/useUserRating.ts @@ -0,0 +1,30 @@ +import { composeUserRatingURL, initialUserRating, processUserRating } from '../../api/userRating' +import { UserRating, isUserRatingResponse } from '../../api/userRating/types' +import useFetch, { UseFetchReturn } from '../useFetch' + +const useUserRating = (userId: number): UseFetchReturn => ( + // useFetch( + // composeUserRatingURL(userId), + // 'GET', + // false, + // isUserRatingResponse, + // processUserRating, + // initialUserRating + // ) + + { + data: 3, + loading: false, + error: null, + refetch: () => { return }, + } + + // { + // data: undefined, + // loading: true, + // error: null, + // refetch: () => { return }, + // } +) + +export default useUserRating diff --git a/front/src/styles/Rating.module.css b/front/src/styles/Rating.module.css new file mode 100644 index 0000000..48a57c7 --- /dev/null +++ b/front/src/styles/Rating.module.css @@ -0,0 +1,19 @@ +.star { + -webkit-text-stroke: 2px var(--bs-body-color); + font-size: 1.5rem; + color: var(--bs-modal-bg); + background: none; + border: none; + padding: 0 3px; + transition: 0.15s ease-in-out; +} + +.starFilled { + color: var(--bs-body-color); +} + +.starSelected { + color: var(--bs-success); + -webkit-text-stroke: 2px var(--bs-success); + transform: scale(1.3); +} \ No newline at end of file From 2b5a917107df4a6ccd11ec23e1bd5379ac10449e Mon Sep 17 00:00:00 2001 From: dm1sh Date: Mon, 7 Aug 2023 14:10:52 +0300 Subject: [PATCH 34/37] Minor api fixes, made useId hook login optional --- front/src/api/user/index.ts | 2 +- front/src/hooks/api/useUser.ts | 2 +- front/src/hooks/useId.ts | 4 ++-- front/src/hooks/useSend.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/front/src/api/user/index.ts b/front/src/api/user/index.ts index 7354125..9f489b4 100644 --- a/front/src/api/user/index.ts +++ b/front/src/api/user/index.ts @@ -17,7 +17,7 @@ const initialUser: User = import.meta.env.DEV ? { // Temporary, until api is rea } const composeUserURL = () => ( - API_URL + '/user?' + API_URL + '/users/me?' ) const processUser = (data: UserResponse): User => { diff --git a/front/src/hooks/api/useUser.ts b/front/src/hooks/api/useUser.ts index 89be949..758ba8f 100644 --- a/front/src/hooks/api/useUser.ts +++ b/front/src/hooks/api/useUser.ts @@ -4,7 +4,7 @@ import { UseFetchReturn } from '../useFetch' const useUser = (): UseFetchReturn => ( // useFetch( - // composeUserUrl(getToken()), + // composeUserURL(), // 'GET', // true, // isUserResponse, diff --git a/front/src/hooks/useId.ts b/front/src/hooks/useId.ts index 4145876..c7b28c1 100644 --- a/front/src/hooks/useId.ts +++ b/front/src/hooks/useId.ts @@ -2,12 +2,12 @@ import { useNavigate } from 'react-router-dom' import { getId } from '../utils/auth' -function useId() { +function useId(require = false) { const navigate = useNavigate() const id = getId() - if (id < 0) { + if (require && id < 0) { navigate('/login') } diff --git a/front/src/hooks/useSend.ts b/front/src/hooks/useSend.ts index 8d720d8..4e925cf 100644 --- a/front/src/hooks/useSend.ts +++ b/front/src/hooks/useSend.ts @@ -81,7 +81,7 @@ function useSend>( } if (import.meta.env.DEV) { - console.log(url, params, err) + console.error(url, params, err) } } From e1e1244b3a73e83b395a32a34b8f1d8c62a5706c Mon Sep 17 00:00:00 2001 From: dm1sh Date: Tue, 8 Aug 2023 00:59:26 +0300 Subject: [PATCH 35/37] Updated sign up interface --- front/src/api/signup/index.ts | 12 ++---------- front/src/api/signup/types.ts | 9 +-------- front/src/api/token/index.ts | 12 +----------- front/src/components/AuthForm.tsx | 24 +++++------------------- front/src/hooks/api/useSignUp.ts | 9 +++------ 5 files changed, 12 insertions(+), 54 deletions(-) diff --git a/front/src/api/signup/index.ts b/front/src/api/signup/index.ts index 0a6d102..b1d12e7 100644 --- a/front/src/api/signup/index.ts +++ b/front/src/api/signup/index.ts @@ -1,18 +1,10 @@ import { API_URL } from '../../config' -import { fallbackTo, isString } from '../../utils/types' -import { SignUp, SignUpBody, SignUpResponse } from './types' +import { SignUp, SignUpResponse } from './types' const composeSignUpURL = () => ( API_URL + '/signup?' ) -const composeSignUpBody = (formData: FormData): SignUpBody => ({ - email: fallbackTo(formData.get('email'), isString, ''), - password: fallbackTo(formData.get('password'), isString, ''), - name: fallbackTo(formData.get('name'), isString, ''), - surname: fallbackTo(formData.get('surname'), isString, ''), -}) - const processSignUp = (data: SignUpResponse): SignUp => { if (!data.Success) { throw new Error(data.Message) @@ -21,4 +13,4 @@ const processSignUp = (data: SignUpResponse): SignUp => { return true } -export { composeSignUpURL, composeSignUpBody, processSignUp } +export { composeSignUpURL, processSignUp } diff --git a/front/src/api/signup/types.ts b/front/src/api/signup/types.ts index cd22da8..02dd310 100644 --- a/front/src/api/signup/types.ts +++ b/front/src/api/signup/types.ts @@ -1,12 +1,5 @@ import { isConst, isObject } from '../../utils/types' -type SignUpBody = { - email: string, - password: string, - name: string, - surname: string, -} - type SignUpResponse = { Success: true, } | { @@ -25,6 +18,6 @@ const isSignUpResponse = (obj: unknown): obj is SignUpResponse => ( type SignUp = boolean -export type { SignUpBody, SignUpResponse, SignUp } +export type { SignUpResponse, SignUp } export { isSignUpResponse } diff --git a/front/src/api/token/index.ts b/front/src/api/token/index.ts index 01072d2..2c14666 100644 --- a/front/src/api/token/index.ts +++ b/front/src/api/token/index.ts @@ -1,22 +1,12 @@ import { API_URL } from '../../config' -import { fallbackTo, isString } from '../../utils/types' import { Token, TokenResponse } from './types' const composeTokenURL = () => ( API_URL + '/token?' ) -const composeSignInBody = (formData: FormData) => { - const resFD = new FormData() - - resFD.append('username', fallbackTo(formData.get('email'), isString, '')) - resFD.append('password', fallbackTo(formData.get('password'), isString, '')) - - return resFD -} - const processToken = (data: TokenResponse): Token => { return data.access_token } -export { composeTokenURL, composeSignInBody, processToken } +export { composeTokenURL, processToken } diff --git a/front/src/components/AuthForm.tsx b/front/src/components/AuthForm.tsx index 8cb45ea..0b33953 100644 --- a/front/src/components/AuthForm.tsx +++ b/front/src/components/AuthForm.tsx @@ -2,8 +2,6 @@ import { FormEventHandler, useCallback } from 'react' import { Button, Form } from 'react-bootstrap' import { useSignIn, useSignUp } from '../hooks/api' -import { composeSignUpBody } from '../api/signup' -import { composeSignInBody } from '../api/token' type AuthFormProps = { register: boolean, @@ -23,11 +21,11 @@ function AuthForm({ goBack, register }: AuthFormProps) { void (async () => { const accountCreated = register ? ( - await handleSignUp(composeSignUpBody(formData)) + await handleSignUp(formData) ) : true if (accountCreated) { - if (await handleSignIn(composeSignInBody(formData))) { + if (await handleSignIn(formData)) { goBack() } } @@ -37,23 +35,11 @@ function AuthForm({ goBack, register }: AuthFormProps) { return (
    - - Почта - + + Как вас называть? + - {register && <> - - Имя - - - - - Фамилия - - - } - Пароль diff --git a/front/src/hooks/api/useSignUp.ts b/front/src/hooks/api/useSignUp.ts index 374defc..81bf3ab 100644 --- a/front/src/hooks/api/useSignUp.ts +++ b/front/src/hooks/api/useSignUp.ts @@ -1,6 +1,6 @@ import { useSendWithButton } from '..' import { composeSignUpURL, processSignUp } from '../../api/signup' -import { SignUpBody, isSignUpResponse } from '../../api/signup/types' +import { isSignUpResponse } from '../../api/signup/types' function useSignUp() { const { doSend, button } = useSendWithButton( @@ -14,12 +14,9 @@ function useSignUp() { processSignUp, ) - async function handleSignUp(data: SignUpBody) { + async function handleSignUp(formData: FormData) { const res = await doSend({}, { - body: JSON.stringify(data), - headers: { - 'Content-Type': 'application/json', - }, + body: formData, }) return res ?? false From c52a62390752882629814274681406dd66dcd44c Mon Sep 17 00:00:00 2001 From: dm1sh Date: Tue, 8 Aug 2023 12:07:12 +0300 Subject: [PATCH 36/37] Updated announcement fetching to new response schema --- front/src/api/announcement/types.ts | 4 ++-- front/src/api/announcements/index.ts | 2 +- front/src/api/announcements/types.ts | 12 +++--------- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/front/src/api/announcement/types.ts b/front/src/api/announcement/types.ts index 945477f..449fc6f 100644 --- a/front/src/api/announcement/types.ts +++ b/front/src/api/announcement/types.ts @@ -6,7 +6,7 @@ type AnnouncementResponse = { user_id: number, name: string, category: Category, - best_by: number, + best_by: string, address: string, longtitude: number, latitude: number, @@ -23,7 +23,7 @@ const isAnnouncementResponse = (obj: unknown): obj is AnnouncementResponse => ( 'user_id': 'number', 'name': 'string', 'category': isCategory, - 'best_by': 'number', + 'best_by': 'string', 'address': 'string', 'longtitude': 'number', 'latitude': 'number', diff --git a/front/src/api/announcements/index.ts b/front/src/api/announcements/index.ts index 1946d5c..22889e7 100644 --- a/front/src/api/announcements/index.ts +++ b/front/src/api/announcements/index.ts @@ -11,7 +11,7 @@ const composeAnnouncementsURL = (filters: FiltersType) => ( ) const processAnnouncements = (data: AnnouncementsResponse): Announcement[] => ( - data.list_of_announcements.map(processAnnouncement) + data.map(processAnnouncement) ) export { initialAnnouncements, composeAnnouncementsURL, processAnnouncements } diff --git a/front/src/api/announcements/types.ts b/front/src/api/announcements/types.ts index 6c3b89e..37e86cc 100644 --- a/front/src/api/announcements/types.ts +++ b/front/src/api/announcements/types.ts @@ -1,16 +1,10 @@ -import { isArrayOf, isObject } from '../../utils/types' +import { isArrayOf } from '../../utils/types' import { AnnouncementResponse, isAnnouncementResponse } from '../announcement/types' -type AnnouncementsResponse = { - list_of_announcements: AnnouncementResponse[], - Success: boolean, -} +type AnnouncementsResponse = AnnouncementResponse[] const isAnnouncementsResponse = (obj: unknown): obj is AnnouncementsResponse => ( - isObject(obj, { - 'list_of_announcements': obj => isArrayOf(obj, isAnnouncementResponse), - 'Success': 'boolean', - }) + isArrayOf(obj, isAnnouncementResponse) ) export type { From 9a4226dc30c995f5e3a44c945ccadbe5e65d6bd5 Mon Sep 17 00:00:00 2001 From: dm1sh Date: Tue, 8 Aug 2023 12:20:47 +0300 Subject: [PATCH 37/37] New auth form design, fixed useSend token sending --- front/src/api/signup/index.ts | 8 ++++- front/src/components/AuthForm.tsx | 57 +++++++++++++++++++------------ front/src/hooks/api/useSignIn.ts | 2 +- front/src/hooks/api/useSignUp.ts | 2 +- front/src/hooks/useSend.ts | 2 +- front/src/pages/LoginPage.tsx | 30 ++-------------- 6 files changed, 48 insertions(+), 53 deletions(-) diff --git a/front/src/api/signup/index.ts b/front/src/api/signup/index.ts index b1d12e7..003518d 100644 --- a/front/src/api/signup/index.ts +++ b/front/src/api/signup/index.ts @@ -5,6 +5,12 @@ const composeSignUpURL = () => ( API_URL + '/signup?' ) +const composeSignUpBody = (formData: FormData) => { + formData.append('nickname', formData.get('username') ?? '') + + return formData +} + const processSignUp = (data: SignUpResponse): SignUp => { if (!data.Success) { throw new Error(data.Message) @@ -13,4 +19,4 @@ const processSignUp = (data: SignUpResponse): SignUp => { return true } -export { composeSignUpURL, processSignUp } +export { composeSignUpURL, composeSignUpBody, processSignUp } diff --git a/front/src/components/AuthForm.tsx b/front/src/components/AuthForm.tsx index 0b33953..3f1dd89 100644 --- a/front/src/components/AuthForm.tsx +++ b/front/src/components/AuthForm.tsx @@ -1,14 +1,14 @@ import { FormEventHandler, useCallback } from 'react' -import { Button, Form } from 'react-bootstrap' +import { Button, ButtonGroup, Form } from 'react-bootstrap' import { useSignIn, useSignUp } from '../hooks/api' +import { composeSignUpBody } from '../api/signup' type AuthFormProps = { - register: boolean, goBack: () => void, } -function AuthForm({ goBack, register }: AuthFormProps) { +const AuthForm = ({ goBack }: AuthFormProps) => { const { handleSignUp, signUpButton } = useSignUp() const { handleSignIn, signInButton } = useSignIn() @@ -19,9 +19,11 @@ function AuthForm({ goBack, register }: AuthFormProps) { const formData = new FormData(e.currentTarget) + const register = (e.nativeEvent as SubmitEvent).submitter?.id === 'register' + void (async () => { const accountCreated = register ? ( - await handleSignUp(formData) + await handleSignUp(composeSignUpBody(formData)) ) : true if (accountCreated) { @@ -31,34 +33,45 @@ function AuthForm({ goBack, register }: AuthFormProps) { } })() - }, [register, goBack, handleSignUp, handleSignIn]) + }, [goBack, handleSignUp, handleSignIn]) return ( - Как вас называть? - + Как меня называть + - Пароль - + И я могу доказать, что это я + - {register && - - - - - Я согласен с условиями обработки персональных данных - - - - } + + + + + Я согласен с условиями обработки персональных данных + + + -