瀏覽代碼

31.03.2023 17:17

Volddemar4ik 1 年之前
父節點
當前提交
b241a10599

+ 260 - 3
js/Project/project/src/components/search/index.js

@@ -1,7 +1,264 @@
+import { url } from "../../App";
+
+import React, { useEffect, useState } from "react"
+import { Box, Typography, Avatar, Stack, InputBase, List, ListItem, ListItemAvatar, ListItemText, Paper, CircularProgress } from '@mui/material';
+import Button from '@mui/material-next/Button';
+import { SearchRounded, TouchAppRounded } from '@mui/icons-material';
+import { useDispatch, useSelector } from "react-redux";
+
+import { actionFullSearch } from "../../redux/thunks";
+import { useHistory } from "react-router-dom";
+
+
+
 export default function Search() {
+
+    const dispatch = useDispatch()
+
+    const searchUserLogin = useSelector(state => state?.search?.userLogin?.payload)
+    const searchUserNick = useSelector(state => state?.search?.userNick?.payload)
+    const searchPostTitle = useSelector(state => state?.search?.postTitle?.payload)
+    const searchPostText = useSelector(state => state?.search?.postText?.payload)
+    const searchCommenttext = useSelector(state => state?.search?.commetText?.payload)
+
+    const searchUserLoginStatus = useSelector(state => state?.search?.userLogin?.status)
+    const searchUserNickStatus = useSelector(state => state?.search?.userNick?.status)
+    const searchPostTitleStatus = useSelector(state => state?.search?.postTitle?.status)
+    const searchPostTextStatus = useSelector(state => state?.search?.postText?.status)
+    const searchCommenttextStatus = useSelector(state => state?.search?.commetText?.status)
+
+    // отслеживаем статус загрузки всех промисов
+    const [downloadStatus, setDownloadStatus] = useState(true)
+    console.log('downloadStatus: ', downloadStatus)
+
+    useEffect(() => {
+        setDownloadStatus(
+            (searchUserLoginStatus === 'FULFILLED') &&
+            (searchUserNickStatus === 'FULFILLED') &&
+            (searchPostTitleStatus === 'FULFILLED') &&
+            (searchPostTextStatus === 'FULFILLED') &&
+            (searchCommenttextStatus === 'FULFILLED')
+        )
+    },
+        [searchUserLoginStatus, searchUserNickStatus, searchPostTitleStatus, searchPostTextStatus, searchCommenttextStatus])
+
+
+    // собираем все в один массив после завершения поиска
+    const [fullSearchData, setFullSearchData] = useState([])
+    // console.log('fullSearchData; ', fullSearchData?.length)
+
+    useEffect(() => {
+        setFullSearchData((searchUserLogin && searchUserNick && searchPostTitle && searchPostText && searchCommenttext) && [...new Map([...searchUserLogin, ...searchUserNick, ...searchPostTitle, ...searchPostText, ...searchCommenttext]?.map(item => [item?._id, item])).values()])
+    },
+        [searchUserLogin, searchUserNick, searchPostTitle, searchPostText, searchCommenttext])
+
+
+    // отслеживание текста в поле ввода
+    const [inputText, setInputText] = useState('')
+
+
+    // отслеживание нажатия enter на кнопку поиска
+    const searchButtonClick = document.getElementById('searchField');
+    searchButtonClick && searchButtonClick.addEventListener('keydown', function (e) {
+        if (e.code === 'Enter') {
+            e.preventDefault()
+            onSearching()
+        }
+    })
+
+
+    // отправка запроса на поиск
+    function onSearching() {
+        // собираем регулярку
+        const newText = (new RegExp(inputText.split(' ').join('|'))).toString()
+
+        console.log('reg: ', newText)
+
+        dispatch(actionFullSearch(newText))
+    }
+
+
+
     return (
-        <div>
-            тут будет страница поиска
-        </div>
+        <Box
+            sx={{
+                padding: '16px',
+                width: '80%',
+                margin: '0 auto',
+            }}
+        >
+            <Stack spacing={2}>
+                <Box sx={{
+                    border: '1px solid #E8E8E8',
+                    borderRadius: '8px',
+                    padding: '8px'
+                }}
+                >
+                    <InputBase
+
+                        fullWidth={true}
+                        placeholder="Найти..."
+                        value={inputText}
+                        onChange={e => setInputText(e.target.value)}
+                        inputProps={{
+                            'aria-label': 'Найти...',
+                            id: 'searchField'
+                        }}
+                        sx={{
+                            ml: 1, flex: 1
+                        }}
+                        startAdornment={
+                            <SearchRounded position="start" />
+                        }
+
+                        endAdornment={
+                            <Typography
+                                variant='subtitle2'
+                                align='right'
+                                color='primary.main'
+                                sx={{
+                                    alignSelf: 'center',
+                                    cursor: 'pointer',
+                                    margin: '0 8px',
+                                    fontWeight: '700'
+                                }}>
+                                {inputText !== '' &&
+                                    <Box
+                                        onClick={onSearching}
+                                    >
+                                        <Button
+                                            startIcon={<TouchAppRounded />}
+                                            color="primary"
+                                            size="small"
+                                            variant="filledTonal"
+                                            sx={{
+                                                padding: '5px 10px'
+                                            }}
+                                        >
+                                            Искать
+                                        </Button>
+                                    </Box>
+                                }
+                            </Typography>
+                        }
+                    />
+                </Box>
+
+                {!downloadStatus &&
+                    <Box
+                        display="flex"
+                        justifyContent="center"
+                        alignItems="center"
+                        height="20vh"
+                    >
+                        <CircularProgress />
+                    </Box>}
+
+                <Box>
+                    <List
+                        sx={{
+                            bgcolor: 'background.paper',
+                            width: '90%',
+                            margin: '0 auto'
+                        }}>
+                        {fullSearchData && fullSearchData?.map(item => <SearchCard data={item} key={item?._id} />)}
+                    </List>
+                </Box>
+            </Stack >
+
+            {fullSearchData && fullSearchData?.length === 0 &&
+                <Typography
+                    variant="subtitle1"
+                    gutterBottom
+                    color='text.primary'
+                    align='center'
+                >
+                    К сожалению, нам не удалось ничего найти по этому запросу.
+                </Typography>}
+        </Box>
+    )
+}
+
+
+function SearchCard({ data }) {
+    const history = useHistory()
+
+    function toPost() {
+        if (data?.title) history.push(`/post/${data?._id}`)
+
+        if (data?.post) history.push(`/post/${data?.post?._id}`)
+    }
+
+    function toUser() {
+        history.push(`/user/${data?.owner ? data?.owner?._id : data?._id}`)
+    }
+
+
+    return (
+        <Paper
+            elevation={3}
+            sx={{
+                padding: '8px',
+                marginTop: '10px'
+            }}
+        >
+
+            <ListItem
+                alignItems="flex-start"
+                sx={{
+                    cursor: 'pointer',
+                    width: '100%',
+                    padding: '0 16px'
+                }}
+                onClick={toUser}
+            >
+                <ListItemAvatar>
+                    <Avatar
+                        alt={data?.owner ? data?.owner?.login : data?.login}
+                        src={url + (data?.owner ? data?.owner?.avatar?.url : data?.avatar?.url)} />
+                </ListItemAvatar>
+
+                <ListItemText
+                    primary={data?.owner ? data?.owner?.login : data?.login}
+                    secondary={
+                        <Typography
+                            sx={{ display: 'inline' }}
+                            component="span"
+                            variant="body2"
+                            color="text.primary"
+                        >
+                            {data?.owner ? data?.owner?.nick : data?.nick}
+                        </Typography>
+                    }
+                />
+
+            </ListItem>
+
+            {data?.owner &&
+                <ListItem
+                    sx={{
+                        border: '1px solid #E8E8E8',
+                        borderRadius: '8px',
+                        marginTop: '8px',
+                        cursor: 'pointer'
+                    }}
+                    onClick={toPost}
+                >
+                    <ListItemText
+                        primary={data?.title && data?.title}
+                        secondary={data?.text &&
+                            <Typography
+                                sx={{ display: 'inline' }}
+                                component="span"
+                                variant="body2"
+                                color="text.primary"
+                            >
+                                {data?.text}
+                            </Typography>
+                        }
+                    />
+                </ListItem>}
+
+        </Paper>
     )
 }

+ 266 - 0
js/Project/project/src/components/search/test.js

@@ -0,0 +1,266 @@
+import { url } from "../../App";
+
+import React, { useEffect, useState } from "react"
+import { Tabs, Tab, TabPanel, Box, Typography, CardHeader, Avatar, Stack, FormControl, InputLabel, OutlinedInput, InputAdornment, IconButton, InputBase, Grid, List, ListItem, ListItemAvatar, ListItemText, Divider, Paper, CircularProgress } from '@mui/material';
+import Button from '@mui/material-next/Button';
+import PropTypes from 'prop-types';
+import { PersonSearchRounded, ForumRounded, CollectionsRounded, SearchRounded, TouchAppRounded } from '@mui/icons-material';
+import { useDispatch, useSelector } from "react-redux";
+
+import { actionFullSearch } from "../../redux/thunks";
+import { useHistory } from "react-router-dom";
+
+
+
+export default function Search() {
+
+    const dispatch = useDispatch()
+
+    const searchUserLogin = useSelector(state => state?.search?.userLogin?.payload)
+    const searchUserNick = useSelector(state => state?.search?.userNick?.payload)
+    const searchPostTitle = useSelector(state => state?.search?.postTitle?.payload)
+    const searchPostText = useSelector(state => state?.search?.postText?.payload)
+    const searchCommenttext = useSelector(state => state?.search?.commetText?.payload)
+
+    const searchUserLoginStatus = useSelector(state => state?.search?.userLogin?.status)
+    const searchUserNickStatus = useSelector(state => state?.search?.userNick?.status)
+    const searchPostTitleStatus = useSelector(state => state?.search?.postTitle?.status)
+    const searchPostTextStatus = useSelector(state => state?.search?.postText?.status)
+    const searchCommenttextStatus = useSelector(state => state?.search?.commetText?.status)
+
+    // отслеживаем статус загрузки всех промисов
+    const [downloadStatus, setDownloadStatus] = useState(false)
+    console.log('downloadStatus: ', downloadStatus)
+
+    useEffect(() => {
+        setDownloadStatus(
+            (searchUserLoginStatus === 'FULFILLED') &&
+            (searchUserNickStatus === 'FULFILLED') &&
+            (searchPostTitleStatus === 'FULFILLED') &&
+            (searchPostTextStatus === 'FULFILLED') &&
+            (searchCommenttextStatus === 'FULFILLED')
+        )
+    },
+        [searchUserLoginStatus, searchUserNickStatus, searchPostTitleStatus, searchPostTextStatus, searchCommenttextStatus])
+
+
+    // собираем все в один массив после завершения поиска
+    const [fullSearchData, setFullSearchData] = useState([])
+    // console.log('fullSearchData; ', fullSearchData?.length)
+
+    useEffect(() => {
+        setFullSearchData((searchUserLogin && searchUserNick && searchPostTitle && searchPostText && searchCommenttext) && [...new Map([...searchUserLogin, ...searchUserNick, ...searchPostTitle, ...searchPostText, ...searchCommenttext]?.map(item => [item?._id, item])).values()])
+    },
+        [searchUserLogin, searchUserNick, searchPostTitle, searchPostText, searchCommenttext])
+
+
+    // отслеживание состояния для элемента статус-бара
+    const [isProgress, setIsProgress] = useState(downloadStatus)
+    // console.log('isProgress: ', isProgress)
+
+
+    // отслеживание текста в поле ввода
+    const [inputText, setInputText] = useState('')
+
+
+    // отслеживание нажатия enter на кнопку поиска
+    const searchButtonClick = document.getElementById('searchField');
+    searchButtonClick && searchButtonClick.addEventListener('keydown', function (e) {
+        if (e.code === 'Enter') {
+            e.preventDefault()
+            onSearching()
+        }
+    })
+
+
+    // отправка запроса на поиск
+    async function onSearching() {
+        // setIsProgress(!isProgress)
+
+        const newText = (new RegExp(inputText.split(' ').join('|'))).toString()
+
+        const res = await dispatch(actionFullSearch(newText))
+
+        // console.log(22, res)
+
+        // setIsProgress(!isProgress)
+    }
+
+
+
+    return (
+        <Box
+            sx={{
+                padding: '16px',
+                width: '80%',
+                margin: '0 auto',
+            }}
+        >
+            <Stack spacing={2}>
+                <Box sx={{
+                    border: '1px solid #E8E8E8',
+                    borderRadius: '8px',
+                    padding: '8px'
+                }}
+                >
+                    <InputBase
+
+                        fullWidth={true}
+                        placeholder="Найти..."
+                        value={inputText}
+                        onChange={e => setInputText(e.target.value)}
+                        inputProps={{
+                            'aria-label': 'Найти...',
+                            id: 'searchField'
+                        }}
+                        sx={{
+                            ml: 1, flex: 1
+                        }}
+                        startAdornment={
+                            <SearchRounded position="start" />
+                        }
+
+                        endAdornment={
+                            <Typography
+                                variant='subtitle2'
+                                align='right'
+                                color='primary.main'
+                                sx={{
+                                    alignSelf: 'center',
+                                    cursor: 'pointer',
+                                    margin: '0 8px',
+                                    fontWeight: '700'
+                                }}>
+                                {inputText !== '' &&
+                                    <Box
+                                        onClick={onSearching}
+                                    >
+                                        <Button
+                                            startIcon={<TouchAppRounded />}
+                                            color="primary"
+                                            size="small"
+                                            variant="filledTonal"
+                                            sx={{
+                                                padding: '5px 10px'
+                                            }}
+                                        >
+                                            Искать
+                                        </Button>
+                                    </Box>
+                                }
+                            </Typography>
+                        }
+                    />
+                </Box>
+
+
+                {/* {!downloadStatus
+                    ? isProgress && <CircularProgress />
+                    : */}
+                <Box>
+                    <List
+                        sx={{
+                            bgcolor: 'background.paper',
+                            width: '90%',
+                            margin: '0 auto'
+                        }}>
+                        {fullSearchData && fullSearchData?.map(item => <SearchCard data={item} key={item?._id} />)}
+                    </List>
+                </Box>
+                {/* } */}
+
+
+
+            </Stack >
+
+            {fullSearchData && fullSearchData?.length === 0 &&
+                <Typography>
+                    нихера не нашло
+                </Typography>}
+        </Box>
+    )
+}
+
+
+function SearchCard({ data }) {
+    const history = useHistory()
+
+    function toPost() {
+        if (data?.title) history.push(`/post/${data?._id}`)
+
+        if (data?.post) history.push(`/post/${data?.post?._id}`)
+    }
+
+    function toUser() {
+        history.push(`/user/${data?.owner ? data?.owner?._id : data?._id}`)
+    }
+
+
+    return (
+        <Paper
+            elevation={3}
+            sx={{
+                padding: '8px',
+                marginTop: '10px'
+            }}
+        >
+
+            <ListItem
+                alignItems="flex-start"
+                sx={{
+                    cursor: 'pointer',
+                    width: '100%',
+                    padding: '0 16px'
+                }}
+                onClick={toUser}
+            >
+                <ListItemAvatar>
+                    <Avatar
+                        alt={data?.owner ? data?.owner?.login : data?.login}
+                        src={url + (data?.owner ? data?.owner?.avatar?.url : data?.avatar?.url)} />
+                </ListItemAvatar>
+
+                <ListItemText
+                    primary={data?.owner ? data?.owner?.login : data?.login}
+                    secondary={
+                        <Typography
+                            sx={{ display: 'inline' }}
+                            component="span"
+                            variant="body2"
+                            color="text.primary"
+                        >
+                            {data?.owner ? data?.owner?.nick : data?.nick}
+                        </Typography>
+                    }
+                />
+
+            </ListItem>
+
+            {data?.owner &&
+                <ListItem
+                    sx={{
+                        border: '1px solid #E8E8E8',
+                        borderRadius: '8px',
+                        marginTop: '8px',
+                        cursor: 'pointer'
+                    }}
+                    onClick={toPost}
+                >
+                    <ListItemText
+                        primary={data?.title && data?.title}
+                        secondary={data?.text &&
+                            <Typography
+                                sx={{ display: 'inline' }}
+                                component="span"
+                                variant="body2"
+                                color="text.primary"
+                            >
+                                {data?.text}
+                            </Typography>
+                        }
+                    />
+                </ListItem>}
+
+        </Paper>
+    )
+}

+ 21 - 7
js/Project/project/src/components/structure/header.js

@@ -6,7 +6,7 @@ import { AppBar, Box, Toolbar, Typography, Menu, Container, Avatar, Tooltip, Men
 import Button from '@mui/material-next/Button';
 import { styled } from '@mui/material/styles';
 import { AddAPhotoRounded, SearchRounded, LogoutRounded, ManageAccountsRounded, TouchAppRounded } from '@mui/icons-material';
-import { actionFullLogout } from '../../redux/thunks';
+import { actionFullLogout, actionFullSearch } from '../../redux/thunks';
 
 import { url } from '../../App';
 
@@ -64,8 +64,6 @@ function UserMenu({ user }) {
     // Thunk для разлогина и редирект на главную, потому что остальные запросы не работают, если нет токена
     async function onLogout() {
         dispatch(actionFullLogout())
-
-        history.push('/')
     }
 
     function toCreatepost() {
@@ -154,15 +152,30 @@ function UserMenu({ user }) {
 
 function Header({ login }) {
     let history = useHistory()
+    const dispatch = useDispatch()
+    const location = useLocation().pathname === ('/search')
 
     // отслеживание введенного текста в поиске
     const [searchText, setSearchText] = useState('')
 
-    // функция поиска
-    function toSearch() {
 
+    // отслеживание нажатия enter на кнопку поиска
+    const searchButtonClick = document.getElementById('searchFieldHeader');
+    searchButtonClick && searchButtonClick.addEventListener('keydown', function (e) {
+        if (e.code === 'Enter') {
+            e.preventDefault()
+            onSearching()
+        }
+    })
 
+    // функция поиска
+    function onSearching() {
         history.push('/search')
+
+        // собираем регулярку
+        const newText = (new RegExp(searchText.split(' ').join('|'))).toString()
+
+        dispatch(actionFullSearch(newText))
     }
 
 
@@ -215,7 +228,7 @@ function Header({ login }) {
                                 </Typography>
                             </Stack>
 
-                            {login &&
+                            {login && !location &&
                                 <Box sx={{
                                     position: 'relative',
                                     marginLeft: 0
@@ -243,6 +256,7 @@ function Header({ login }) {
                                         InputProps={{
                                             'disableUnderline': true,
                                             'aria-label': 'search',
+                                            id: 'searchFieldHeader',
                                             endAdornment:
                                                 <Typography
                                                     variant='subtitle2'
@@ -256,7 +270,7 @@ function Header({ login }) {
                                                     }}>
                                                     {searchText !== '' &&
                                                         <Box
-                                                            onClick={toSearch}
+                                                            onClick={onSearching}
                                                         >
                                                             <Button
                                                                 startIcon={<TouchAppRounded />}

+ 6 - 7
js/Project/project/src/redux/action.js

@@ -417,19 +417,18 @@ export const actionUpdateProfile = params => actionPromise('PROMISE', 'UpdatePro
 
 
 // запрос поиска по юзерам
-export const actionSearchUser = params => actionPromise('SEARCH', 'Search', gql(`query UserSearch($findUser: String){
+export const actionSearchUser = (params, name) => actionPromise('SEARCH', name, gql(`query UserSearch($findUser: String){
   UserFind(query: $findUser){
       _id login nick avatar{
         _id url
       }
   }
 }`, {
-  findUser: JSON.stringify([{ params }])
-  // пример запроса: [{login:/владимир|voldde/}]
+  findUser: JSON.stringify(params)
 }))
 
 // запрос поиска по постам
-export const actionSearchPost = params => actionPromise('SEARCH', 'Search', gql(`query PostSearch($findPost: String){
+export const actionSearchPost = (params, name) => actionPromise('SEARCH', name, gql(`query PostSearch($findPost: String){
   PostFind(query: $findPost){
       _id title text owner{
         _id login nick avatar{
@@ -438,11 +437,11 @@ export const actionSearchPost = params => actionPromise('SEARCH', 'Search', gql(
       }
   }
 }`, {
-  findPost: params
+  findPost: JSON.stringify(params)
 }))
 
 // запрос поиска по клмментам
-export const actionSearchComment = params => actionPromise('SEARCH', 'Search', gql(`query CommentSearch($findComment: String){
+export const actionSearchComment = (params, name) => actionPromise('SEARCH', name, gql(`query CommentSearch($findComment: String){
   CommentFind(query: $findComment){
       _id text
     post{
@@ -456,5 +455,5 @@ export const actionSearchComment = params => actionPromise('SEARCH', 'Search', g
       }
   }
 }`, {
-  findComment: params
+  findComment: JSON.stringify(params)
 }))

+ 0 - 2
js/Project/project/src/redux/reducers.js

@@ -25,8 +25,6 @@ export function authReducer(state = {}, { type, token }) {
     }
 
     if (type === 'AUTH_LOGOUT') {
-        localStorage.clear()
-
         return state = {}
     }
 

+ 11 - 38
js/Project/project/src/redux/thunks.js

@@ -63,7 +63,7 @@ export const actionFullLogout = () =>
     async dispatch => {
         const res = await dispatch(actionAuthLogout())
 
-        return res
+        if (res) localStorage.clear()
     }
 
 
@@ -356,45 +356,18 @@ export const actionUserPageSubscribing = (params, id) =>
 
 
 // запрос на поиск элементов
-export const actionFullSEarch = param =>
+export const actionFullSearch = param =>
     async dispatch => {
-
-        // создаем список сущностей, которые нам нужны
-        const variables = {
-            user: ['login', 'nick'],
-            post: ['title', 'text'],
-            comment: ['text']
-        }
-
-
-        // массив для акшонов
-        let searchPromises = []
-
-        // перебираем variables
-        for (const [key, variable] of Object.entries(variables)) {
-            if (key.toLowerCase() === 'user') {
-                for (const item of variable) {
-                    searchPromises.push(actionSearchUser([{ item: param }]))
-                }
-            }
-
-            if (key.toLowerCase() === 'post') {
-                for (const item of variable) {
-                    searchPromises.push(actionSearchPost([{ item: param }]))
-                }
-            }
-
-            if (key.toLowerCase() === 'comment') {
-                for (const item of variable) {
-                    searchPromises.push(actionSearchComment([{ item: param }]))
-                }
-            }
-        }
-
-        console.log('searchPromises: ', searchPromises)
+        const searchPromises = [
+            actionSearchUser([{ login: param }], 'userLogin'),
+            actionSearchUser([{ nick: param }], 'userNick'),
+            actionSearchPost([{ title: param }], 'postTitle'),
+            actionSearchPost([{ text: param }], 'postText'),
+            actionSearchComment([{ text: param }], 'commetText'),
+        ]
 
         // запускаем все акшоны
-        Promise.all(searchPromises.map(item => dispatch(item)))
-
+        const result = await Promise.all(searchPromises.map(item => dispatch(item)))
 
+        return result
     }