From e229a8855ff2ef72289b3693a5f854013e5e7718 Mon Sep 17 00:00:00 2001 From: kelvin Date: Wed, 19 Oct 2022 21:46:17 +0700 Subject: [PATCH] adjust type definition and related component --- client/src/@types/api/Category/index.d.ts | 23 --- client/src/@types/api/Tag/index.d.ts | 21 +++ client/src/@types/api/Title/index.d.ts | 3 +- client/src/@types/api/index.d.ts | 2 +- .../components/CategorySelect/index.d.ts | 17 -- .../components/TagSearchingBlock/index.d.ts | 6 + client/src/@types/components/index.d.ts | 1 - .../@types/initializations/[entry]/index.d.ts | 1 - .../@types/initializations/[feed]/index.d.ts | 3 +- .../@types/initializations/feeds/index.d.ts | 4 +- client/src/@types/pages/[feed]/index.d.ts | 3 +- .../src/@types/pages/create-feed/index.d.ts | 4 +- client/src/@types/pages/feeds/index.d.ts | 6 +- client/src/pages/[feed]/index.tsx | 79 ++++++--- client/src/pages/create-feed/index.tsx | 16 +- client/src/pages/create-feed/style.less | 90 ++++++++++ client/src/pages/global.less | 116 +++++++++++++ client/src/pages/index.tsx | 159 +++++++++--------- client/src/services/api/Category/index.ts | 9 - client/src/services/api/Tag/index.ts | 4 + client/src/services/api/Title/index.ts | 6 +- client/src/services/api/index.ts | 2 +- .../services/initializations/[entry]/index.ts | 8 +- .../services/initializations/[feed]/index.ts | 7 +- .../services/initializations/feeds/index.ts | 10 +- client/src/services/utils.less | 50 ------ 26 files changed, 405 insertions(+), 245 deletions(-) delete mode 100644 client/src/@types/api/Category/index.d.ts create mode 100644 client/src/@types/api/Tag/index.d.ts delete mode 100644 client/src/@types/components/CategorySelect/index.d.ts create mode 100644 client/src/@types/components/TagSearchingBlock/index.d.ts create mode 100644 client/src/pages/create-feed/style.less create mode 100644 client/src/pages/global.less delete mode 100644 client/src/services/api/Category/index.ts create mode 100644 client/src/services/api/Tag/index.ts delete mode 100644 client/src/services/utils.less diff --git a/client/src/@types/api/Category/index.d.ts b/client/src/@types/api/Category/index.d.ts deleted file mode 100644 index a9bbc11d..00000000 --- a/client/src/@types/api/Category/index.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface CategoryResponseData { - type: 'category_detail', - attributes: { - id: string, - name: string, - is_leaf: boolean, - parent_category: string, - ancestors: string[], - created_at: string, - updated_at: string - } -} - -export interface TrendingCategoriesResponseData { - id: string, - name: string, - is_leaf: boolean, - parent_category: string, - ancestors: string[], - created_at: string, - updated_at: string -} - diff --git a/client/src/@types/api/Tag/index.d.ts b/client/src/@types/api/Tag/index.d.ts new file mode 100644 index 00000000..459b84a2 --- /dev/null +++ b/client/src/@types/api/Tag/index.d.ts @@ -0,0 +1,21 @@ +export interface TagResponseData { + type: 'tag_detail', + attributes: { + _id: string, + name: string, + total_title: number, + popularity_ratio: number, + created_at: string, + updated_at: string + } +} + +export interface TrendingTagsResponseData { + _id: string, + name: string, + total_title: number, + popularity_ratio: number, + created_at: string, + updated_at: string +} + diff --git a/client/src/@types/api/Title/index.d.ts b/client/src/@types/api/Title/index.d.ts index 7c091724..438d1d67 100644 --- a/client/src/@types/api/Title/index.d.ts +++ b/client/src/@types/api/Title/index.d.ts @@ -5,8 +5,7 @@ export interface TitleResponseData { name: string, slug: string, entry_count: number, - category_id: string, - category_ancestors: string[], + tags: string[], opened_by: string, rate: { username: string, diff --git a/client/src/@types/api/index.d.ts b/client/src/@types/api/index.d.ts index daea2830..0ec8f729 100644 --- a/client/src/@types/api/index.d.ts +++ b/client/src/@types/api/index.d.ts @@ -1,5 +1,5 @@ export * from './Title' -export * from './Category' +export * from './Tag' export * from './Entry' export * from './Message' export * from './User' diff --git a/client/src/@types/components/CategorySelect/index.d.ts b/client/src/@types/components/CategorySelect/index.d.ts deleted file mode 100644 index 0b081214..00000000 --- a/client/src/@types/components/CategorySelect/index.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface CategoryData { - id: string - name: string - is_leaf: boolean - ancestors: string[] - created_at: string - updated_at: string -} - -export interface CategorySelectProps { - style?: { [key: string]: string | number } - placeHolder?: string - multiple?: boolean - allowClear?: boolean - defaultValue?: string - onSelect: (id: string | string[], title: React.ReactNode[]) => any -} diff --git a/client/src/@types/components/TagSearchingBlock/index.d.ts b/client/src/@types/components/TagSearchingBlock/index.d.ts new file mode 100644 index 00000000..f5fd9ae4 --- /dev/null +++ b/client/src/@types/components/TagSearchingBlock/index.d.ts @@ -0,0 +1,6 @@ +export interface TagSearchingBlockProps { + tagFilter: string[] + setTagFilter: (tag: string) => void + beforeTagDeSelect: () => void + updateTagFilterList: React.Dispatch> +} \ No newline at end of file diff --git a/client/src/@types/components/index.d.ts b/client/src/@types/components/index.d.ts index 5f00d41a..9fd27013 100644 --- a/client/src/@types/components/index.d.ts +++ b/client/src/@types/components/index.d.ts @@ -1,5 +1,4 @@ export * from './Aggrements' -export * from './CategorySelect' export * from './ImageUpload' export * from './PageHelmet' export * from './SignModal' \ No newline at end of file diff --git a/client/src/@types/initializations/[entry]/index.d.ts b/client/src/@types/initializations/[entry]/index.d.ts index a781049e..6a829e68 100644 --- a/client/src/@types/initializations/[entry]/index.d.ts +++ b/client/src/@types/initializations/[entry]/index.d.ts @@ -4,7 +4,6 @@ import { TitleResponseData, EntryResponseData } from '@/@types/api' export interface EntryPageInitials { title: TitleResponseData, - categoryName: string, averageTitleRate: number, entryData: EntryResponseData, } \ No newline at end of file diff --git a/client/src/@types/initializations/[feed]/index.d.ts b/client/src/@types/initializations/[feed]/index.d.ts index d0839ba2..6391d07e 100644 --- a/client/src/@types/initializations/[feed]/index.d.ts +++ b/client/src/@types/initializations/[feed]/index.d.ts @@ -1,10 +1,9 @@ // Local files -import { TitleResponseData, CategoryResponseData } from '@/@types/api' +import { TitleResponseData } from '@/@types/api' import { EntryAttributes } from '@/@types/pages' export interface FeedPageInitials { titleData: TitleResponseData, - categoryData: CategoryResponseData, featuredEntry: string, averageTitleRate: number, entryList: { diff --git a/client/src/@types/initializations/feeds/index.d.ts b/client/src/@types/initializations/feeds/index.d.ts index 72fe8d62..943d1a45 100644 --- a/client/src/@types/initializations/feeds/index.d.ts +++ b/client/src/@types/initializations/feeds/index.d.ts @@ -1,6 +1,6 @@ // Local files -import { TrendingCategoriesResponseData } from '@/@types/api' +import { TrendingTagsResponseData } from '@/@types/api' export interface FeedsPageInitials { - trendingCategories: TrendingCategoriesResponseData[] + trendingTags: TrendingTagsResponseData[] } \ No newline at end of file diff --git a/client/src/@types/pages/[feed]/index.d.ts b/client/src/@types/pages/[feed]/index.d.ts index a8469f22..6ffcbff9 100644 --- a/client/src/@types/pages/[feed]/index.d.ts +++ b/client/src/@types/pages/[feed]/index.d.ts @@ -1,5 +1,5 @@ // Local files -import { CategoryResponseData, TitleResponseData, EntryResponseData} from '@/@types/api' +import { TitleResponseData, EntryResponseData} from '@/@types/api' export interface EntryAttributes { id: string, @@ -17,7 +17,6 @@ export interface EntryAttributes { export interface FeedHeaderProps { accessToken: string, - categoryData: CategoryResponseData, titleData: TitleResponseData, averageTitleRate: number, userRole: number, diff --git a/client/src/@types/pages/create-feed/index.d.ts b/client/src/@types/pages/create-feed/index.d.ts index b68c427b..0b104923 100644 --- a/client/src/@types/pages/create-feed/index.d.ts +++ b/client/src/@types/pages/create-feed/index.d.ts @@ -2,13 +2,13 @@ export interface CreateTitleFormData { name: string, imageBase64: string, imageFile: string, - categoryId: string, + tags: string, } export interface Step1Props { stepMovementTo: (_step?: string | undefined) => void, setCreateTitleFormData: React.Dispatch, - setReadableCategoryValue: React.Dispatch> + setReadableTagValue: React.Dispatch> } export interface Step2Props { diff --git a/client/src/@types/pages/feeds/index.d.ts b/client/src/@types/pages/feeds/index.d.ts index 514f3741..316944cf 100644 --- a/client/src/@types/pages/feeds/index.d.ts +++ b/client/src/@types/pages/feeds/index.d.ts @@ -3,7 +3,7 @@ export interface FeedList { name: string, slug: string, href: string, - categoryName: string, + tags: string[], entryCount: number, featuredEntry: { id: string, @@ -21,6 +21,6 @@ export interface FeedList { export interface FlowHeaderProps { sortBy: "top" | "hot" | undefined, setSortBy: (val: "top" | "hot" | undefined) => void, - resetCategoryFilter: () => void, - openFilterModal: () => void + resetTagFilter: () => void, + beforeFilterReset: () => void } diff --git a/client/src/pages/[feed]/index.tsx b/client/src/pages/[feed]/index.tsx index 5a6fcb17..b159d4a3 100644 --- a/client/src/pages/[feed]/index.tsx +++ b/client/src/pages/[feed]/index.tsx @@ -1,5 +1,5 @@ // Antd dependencies -import { Modal, Form, Input, Button, Popconfirm, message } from 'antd' +import { Modal, Form, Input, Button, Popconfirm, message, Select, Typography } from 'antd' import { InfoCircleOutlined } from '@ant-design/icons' // Other dependencies @@ -9,16 +9,16 @@ import { useRouter, NextRouter } from 'next/router' import { NextPage } from 'next' // Local files -import { fetchEntriesByTitleId, getAverageTitleRate, updateTitle, deleteTitleImage, updateTitleImage } from '@/services/api' -import { TitleResponseData, CategoryResponseData } from '@/@types/api' -import { API_URL, Guest } from '@/../config/constants' -import { CategorySelect } from '@/components/global/CategorySelect' +import { fetchEntriesByTitleId, getAverageTitleRate, updateTitle, deleteTitleImage, updateTitleImage, searchTagByName } from '@/services/api' +import { TitleResponseData } from '@/@types/api' +import { API_URL } from '@/../config/constants' import { PageHelmet } from '@/components/global/PageHelmet' import { EntryAttributes } from '@/@types/pages' import { getFeedPageInitialValues } from '@/services/initializations' import { FeedPageInitials } from '@/@types/initializations' import { ImageUpload } from '@/components/global/ImageUpload' import { AppLayout } from '@/layouts/AppLayout' +import { Roles } from '@/enums' import NotFoundPage from '@/pages/404' import FeedHeader from '@/components/pages/[feed]/FeedHeader' import FeedEntries from '@/components/pages/[feed]/FeedEntries' @@ -29,12 +29,10 @@ const Feed: NextPage = (props): JSX.Element => { const globalState = useSelector((state: any) => state.global) const userRole = useSelector((state: any) => state.user?.attributes.user.role) - const [title, setTitle]: any = useState(props.titleData) - const [category, setCategory]: any = useState(props.categoryData) + const [title, setTitle] = useState(props.titleData) const [featuredEntry, setFeaturedEntry] = useState(props.featuredEntry) const [keywords, setKeywords] = useState(props.keywords) const [averageTitleRate, setAverageTitleRate] = useState(props.averageTitleRate) - const [updateCategoryId, setUpdateCategoryId] = useState(null) const [entryList, setEntryList] = useState<{ entries: EntryAttributes[], count: number @@ -44,15 +42,47 @@ const Feed: NextPage = (props): JSX.Element => { const [titleImageBlob, setTitleImageBlob] = useState(null) const [titleNotFound, setTitleNotFound] = useState(props.error) + const [tagResult, setTagResult] = useState([]) + const [noTagDataMessage, setNoTagDataMessage] = useState('Enter at least 3 characters to search') + const [tagValue, setTagValue] = useState(title.attributes.tags) + const [form] = Form.useForm() + useEffect(() => { + if (title) handleEntryFetching(10 * (Number(router.query.page) - 1) || 0) + }, [title, sortEntriesBy]) + const handleEntryFetching = (page: number): void => { fetchEntriesByTitleId(title.attributes.id, page, sortEntriesBy).then(async ({ data }) => setEntryList(data.attributes)) } - useEffect(() => { - if (title) handleEntryFetching(10 * (Number(router.query.page) - 1) || 0) - }, [title, sortEntriesBy]) + const handleTagSearching = (value: string): void => { + if (value.length < 3) { + setTagResult([]) + setNoTagDataMessage('Enter at least 3 characters to search') + } + + else { + searchTagByName(value).then(({ data }) => { + const result = [] + data.attributes.tags.map(tag => { + result.push({tag.name}) + }) + + if (result.length !== 0) setTagResult(result) + else setTagResult([{value}]) + }) + } + } + + const handleTagSelect = (value: string): void => { + setTagValue([...tagValue, value]) + } + + const handleDeSelect = (tag: string) => { + const updatedList = tagValue.filter(value => value !== tag) + setTagValue(updatedList) + } const getTitleRate = async (titleId: string): Promise => { await getAverageTitleRate(titleId).then(res => setAverageTitleRate(res.data.attributes.rate || 0)) @@ -60,8 +90,8 @@ const Feed: NextPage = (props): JSX.Element => { const handleTitleUpdate = async (values: { name: string }): Promise => { const updatePayload = { - categoryId: updateCategoryId, - name: values.name + name: values.name, + tags: tagValue, } if (!titleImageBlob) { @@ -85,10 +115,10 @@ const Feed: NextPage = (props): JSX.Element => { } if (titleNotFound) return - if (!entryList || !title || !category || (!averageTitleRate && averageTitleRate !== 0)) return + if (!entryList || !title || (!averageTitleRate && averageTitleRate !== 0)) return return ( - + = (props): JSX.Element => { openUpdateModal={(): void => setUpdateModalVisibility(true)} userRole={userRole} titleData={title} - categoryData={category} averageTitleRate={averageTitleRate} refreshTitleRate={getTitleRate} /> @@ -147,12 +176,20 @@ const Feed: NextPage = (props): JSX.Element => { - setUpdateCategoryId(id)} - /> + placeholder="please specify feed-related keywords" + onSearch={handleTagSearching} + onSelect={handleTagSelect} + onDeselect={handleDeSelect} + notFoundContent={ + {noTagDataMessage} + } + > + {tagResult} + { const accessToken = useSelector((state: any) => state.global.accessToken) @@ -23,7 +23,7 @@ const CreateFeed: React.FC = () => { const [currentStep, setCurrentStep] = useState(0) const [stepComponent, setStepComponent] = useState(null) const [isRequestReady, setIsRequestReady] = useState(false) - const [readableCategoryValue, setReadableCategoryValue] = useState(undefined) + const [readableTagValue, setReadableTagValue] = useState(undefined) const [firstEntryForm, setFirstEntryForm] = useState<{ text: string } | { text: any }>({ text: undefined }) @@ -32,7 +32,7 @@ const CreateFeed: React.FC = () => { name: undefined, imageBase64: undefined, imageFile: undefined, - categoryId: undefined, + tags: undefined, }) const [feedCreatedSuccessfully, setFeedCreatedSuccessfully] = useState(null) @@ -43,7 +43,7 @@ const CreateFeed: React.FC = () => { const titleFormData = new FormData() titleFormData.append('name', createTitleFormData.name) - titleFormData.append('categoryId', createTitleFormData.categoryId) + titleFormData.append('tags', createTitleFormData.tags) if (createTitleFormData.imageFile) titleFormData.append('image', createTitleFormData.imageFile) createTitle(titleFormData, accessToken).then(async (res: AxiosResponse) => { @@ -106,7 +106,7 @@ const CreateFeed: React.FC = () => { } } @@ -115,8 +115,8 @@ const CreateFeed: React.FC = () => { if (!stepComponent) handleStepMovement() return ( - - + + diff --git a/client/src/pages/create-feed/style.less b/client/src/pages/create-feed/style.less new file mode 100644 index 00000000..9cf414b8 --- /dev/null +++ b/client/src/pages/create-feed/style.less @@ -0,0 +1,90 @@ +@import '~antd/es/style/themes/default.less'; + +.card { + margin-bottom: 24px; +} + +.heading { + margin: 0 0 16px; + font-size: 14px; + line-height: 22px; +} + +.steps:global(.ant-steps) { + max-width: 750px; + margin: 16px auto; +} + +.errorIcon { + margin-right: 24px; + color: @error-color; + cursor: pointer; + span.anticon { + margin-right: 4px; + } +} + +.errorPopover { + :global { + .ant-popover-inner-content { + min-width: 256px; + max-height: 290px; + padding: 0; + overflow: auto; + } + } +} + +.errorListItem { + padding: 8px 16px; + list-style: none; + border-bottom: 1px solid @border-color-split; + cursor: pointer; + transition: all 0.3s; + &:hover { + background: @primary-1; + } + &:last-child { + border: 0; + } + .errorIcon { + float: left; + margin-top: 4px; + margin-right: 12px; + padding-bottom: 22px; + color: @error-color; + } + .errorField { + margin-top: 2px; + color: @text-color-secondary; + font-size: 12px; + } +} + +.editable { + td { + padding-top: 13px !important; + padding-bottom: 12.5px !important; + } +} + +// custom footer for fixed footer toolbar +.advancedForm + div { + padding-bottom: 64px; +} + +.advancedForm { + :global { + .ant-form .ant-row:last-child .ant-form-item { + margin-bottom: 24px; + } + .ant-table td { + transition: none !important; + } + } +} + +.optional { + color: @text-color-secondary; + font-style: normal; +} diff --git a/client/src/pages/global.less b/client/src/pages/global.less new file mode 100644 index 00000000..f363c97c --- /dev/null +++ b/client/src/pages/global.less @@ -0,0 +1,116 @@ +@import '~antd/es/style/themes/default.less'; + +@navbar-background: #016d9b !important; +@navbar-font-color: #eee !important; + +html, +body, +#root { + height: 100%; +} + +.ant-layout { + min-height: 100vh !important; + background-color: #eee; +} + +.antBtnLink { + color: rgba(0, 0, 0, 0.65) !important; +} + +.ant-divider-with-text { + border-color: rgba(0, 0, 0, 0.15) !important; +} + +.custom-tag { + line-height: 20px; + cursor: default; + font-size: 14px; + font-weight: bold; + opacity: 0.85; + padding: 0px 5px 0px 5px; + margin: 0px 0px 5px 5px; + border-radius: 4px; + display: inline-table; + font-family: 'Courier New', Courier, monospace; +} + +.blockEdges { + border: solid rgba(0, 0, 0, .4); + border-radius: 5px !important; + border-width: 0.5px; + + .ant-card-head { + border: 0; + } +} + +.ant-tabs > .ant-tabs-nav .ant-tabs-nav-more, .ant-tabs > div > .ant-tabs-nav .ant-tabs-nav-more { + display: none; +} + +canvas { + display: block; +} + +body { + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +ul, +ol { + list-style: none; +} + +:global(.ant-col-xxl-18) { + max-width: 1350px; +} + +#nprogress .bar { + background: #6ec49a !important; +} + +.navBarlogo { + height: 40px; +} + +.ant-menu-submenu { + display: none !important; +} + +.ant-layout-header { + cursor: default; + background-color: @navbar-background; + color: @navbar-font-color; +} + +.ant-layout-header > ul { + background-color: @navbar-background; + color: @navbar-font-color; +} + +@media (max-width: @screen-xs) { + .ant-table { + width: 100%; + overflow-x: auto; + &-thead > tr, + &-tbody > tr { + > th, + > td { + white-space: pre; + > span { + display: block; + } + } + } + } +} + +// Compatible with IE11 +@media screen and(-ms-high-contrast: active), (-ms-high-contrast: none) { + body > .ant-layout { + min-height: 100vh; + } +} diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx index 6016c423..42b00c3c 100644 --- a/client/src/pages/index.tsx +++ b/client/src/pages/index.tsx @@ -1,6 +1,6 @@ // Antd dependencies -import { Button, Card, List, message, BackTop, Row, Col, Typography, Modal, Skeleton } from 'antd' -import { LoadingOutlined, ArrowUpOutlined, EyeOutlined } from '@ant-design/icons' +import { Button, Card, List, message, BackTop, Row, Col, Typography, Modal, Skeleton, Select } from 'antd' +import { LoadingOutlined, ArrowUpOutlined } from '@ant-design/icons' // Other dependencies import React, { useEffect, useState } from 'react' @@ -8,25 +8,26 @@ import { AxiosError, AxiosResponse } from 'axios' import { NextPage } from 'next' import { Img } from 'react-image' import Link from 'next/link' +import stringToColor from 'string-to-color' // Local files -import { fetchAllFeeds, fetchFeaturedEntryByTitleId, fetchTrendingCategories, fetchOneCategory } from '@/services/api' -import { CategorySelect } from '@/components/global/CategorySelect' -import { API_URL, Guest } from '@/../config/constants' +import { fetchAllFeeds, fetchFeaturedEntryByTitleId, fetchTrendingTags } from '@/services/api' +import { API_URL } from '@/../config/constants' import { PageHelmet } from '@/components/global/PageHelmet' import { AdditionalBlock } from '@/components/pages/feeds/AdditionalBlock' -import { TrendingCategoriesResponseData } from '@/@types/api' +import { TrendingTagsResponseData } from '@/@types/api' import { FeedList } from '@/@types/pages/feeds' import { getFeedsPageInitialValues } from '@/services/initializations' import { FeedsPageInitials } from '@/@types/initializations' import { AppLayout } from '@/layouts/AppLayout' import { ArticleListContent } from '@/components/pages/feeds/ArticleListContent' +import { TagSearchingBlock } from '@/components/pages/feeds/TagSearchingBlock' +import { Roles } from '@/enums' import FlowHeader from '@/components/pages/feeds/FlowHeader' const Homepage: NextPage = (props): JSX.Element => { - const [displayFilterModal, setDisplayFilterModal] = useState(false) - const [trendingCategories, setTrendingCategories] = useState(props.trendingCategories) - const [categoryFilter, setCategoryFilter] = useState(undefined) + const [trendingTags, setTrendingTags] = useState(props.trendingTags) + const [tagFilter, setTagFilter] = useState(null) const [feedList, setFeed] = useState([]) const [sortBy, setSortBy] = useState<'hot' | 'top' | undefined>(undefined) const [skipValueForPagination, setSkipValueForPagination] = useState(0) @@ -35,32 +36,50 @@ const Homepage: NextPage = (props): JSX.Element => { const [isLoading, setIsLoading] = useState(true) const [isLoadMoreTriggered, setIsLoadMoreTriggered] = useState(false) - const handleCategoryFilterSet = (id) => { + useEffect(() => { + handleDataFetching().then(() => setIsJustInitialized(false)) + }, []) + + useEffect(() => { + if (!isJustInitialized) { + handleDataFetching() + } + }, [skipValueForPagination, tagFilter, sortBy, isLoading]) + + const handleFlowReset = (): void => { setIsLoading(true) setFeed([]) setSkipValueForPagination(0) - setIsJustInitialized(false) - setCategoryFilter(id) } - const handleSortBySet = (val) => { - setIsLoading(true) - setFeed([]) - setSkipValueForPagination(0) - setIsJustInitialized(false) - setSortBy(val) + const handleSorting = (sortBy: 'hot' | 'top' | undefined): void => { + handleFlowReset() + setSortBy(sortBy) + } + + const handleFilteringTags = (tag: string): void => { + if (tagFilter) { + if (tagFilter.split(',').includes(tag) === false) { + handleFlowReset() + setTagFilter(`${tagFilter},${tag}`) + } + } + + else { + handleFlowReset() + setTagFilter(tag) + } } const handleDataFetching = async (): Promise => { - if (!trendingCategories) { - fetchTrendingCategories() - .then(res => setTrendingCategories(res.data.attributes.categories)) + if (!trendingTags) { + fetchTrendingTags() + .then(res => setTrendingTags(res.data.attributes.tags)) } - await fetchAllFeeds(skipValueForPagination, undefined, categoryFilter, sortBy) + await fetchAllFeeds(skipValueForPagination, undefined, tagFilter, sortBy) .then(async (feedsResponse: AxiosResponse) => { const promises = await feedsResponse.data.attributes.titles.map(async (title: any) => { - const categoryName = await fetchOneCategory(title.category_id).then(({ data }) => data.attributes.name) const featuredEntry: any = await fetchFeaturedEntryByTitleId(title.id).then(featuredEntryResponse => featuredEntryResponse.data.attributes) .catch(_error => { }) @@ -69,7 +88,7 @@ const Homepage: NextPage = (props): JSX.Element => { slug: title.slug, name: title.name, href: `/${title.slug}`, - categoryName: categoryName, + tags: title.tags, createdAt: title.created_at, updatedAt: title.updated_at, entryCount: title.entry_count, @@ -89,13 +108,10 @@ const Homepage: NextPage = (props): JSX.Element => { }) const result = await Promise.all(promises) - - /* TODO - * This is a workaround to fix wrong list order of Feeds Flow. - * Updating feeds should be refactored. - */ - // @ts-ignore - result.map(item => setFeed((feedList: FeedList[]) => [...feedList, item])) + result.map((item: FeedList) => { + if (feedList.find(i => i.id === item.id)) return + setFeed((feedList: FeedList[]) => [...feedList, item]) + }) if (feedsResponse.data.attributes.count > (feedsResponse.data.attributes.titles.length + skipValueForPagination)) setCanLoadMore(true) else setCanLoadMore(false) @@ -167,7 +183,17 @@ const Homepage: NextPage = (props): JSX.Element => { alt="Title Image" /> } - description={

{item.categoryName.toUpperCase()}

} + description={ + item.tags.map(tag => { + return ( +
+ 0xffffff / 2) ? '#000' : '#fff', background: (parseInt(stringToColor(tag).replace('#', ''), 16) > 0xffffff / 2) ? '#fff' : '#000', opacity: 0.9 }}> + #{tag} + +
+ ) + }) + } /> {item.featuredEntry ? @@ -180,9 +206,8 @@ const Homepage: NextPage = (props): JSX.Element => { ) } - const handleTrendingCategoriesRender = (): JSX.Element => { - - if (!trendingCategories) { + const handleTrendingTagsRender = (): JSX.Element => { + if (!trendingTags) { return (
@@ -191,36 +216,23 @@ const Homepage: NextPage = (props): JSX.Element => { } return ( -
- {trendingCategories.map(category => { +
+ {trendingTags.map(tag => { return ( - - - {category.name.toUpperCase()} +
handleFilteringTags(tag.name)} className={'custom-tag'} style={{ backgroundColor: stringToColor(tag.name), cursor: 'pointer' }}> + 0xffffff / 2) ? '#000' : '#fff', background: (parseInt(stringToColor(tag.name).replace('#', ''), 16) > 0xffffff / 2) ? '#fff' : '#000', opacity: 0.9 }}> + #{tag.name} -
) })}
) } - useEffect(() => { - handleDataFetching() - }, [skipValueForPagination, categoryFilter, sortBy, isJustInitialized]) - const handleFetchMore = (): void => { setIsLoadMoreTriggered(true) setSkipValueForPagination(skipValueForPagination + 10) - setIsJustInitialized(false) } const loadMore = canLoadMore && ( @@ -237,27 +249,8 @@ const Homepage: NextPage = (props): JSX.Element => {
) - const handleModalScreen = (): JSX.Element => ( - setDisplayFilterModal(false)} - > - handleCategoryFilterSet(String(id))} - style={{ width: '100%' }} - placeHolder="All Categories" - allowClear - /> - - ) - return ( - + = (props): JSX.Element => { }} > setDisplayFilterModal(true)} - setSortBy={(val: 'top' | 'hot' | undefined): void => handleSortBySet(val)} - resetCategoryFilter={(): void => handleCategoryFilterSet(undefined)} + setSortBy={(val: 'top' | 'hot' | undefined): void => handleSorting(val)} + resetTagFilter={(): void => setTagFilter(null)} + beforeFilterReset={() => handleFlowReset()} sortBy={sortBy} /> - {handleModalScreen()} {handleFeedListView()} - - {handleTrendingCategoriesRender()} + + Trending Tags + {handleTrendingTagsRender()} + handleFlowReset()} + updateTagFilterList={setTagFilter} + /> diff --git a/client/src/services/api/Category/index.ts b/client/src/services/api/Category/index.ts deleted file mode 100644 index e07539be..00000000 --- a/client/src/services/api/Category/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import axios, { AxiosResponse } from 'axios' - -export const fetchOneCategory = (categoryId: string): Promise => axios.get(`/v1/category/${categoryId}`) - -export const fetchMainCategories = (): Promise => axios.get('/v1/category/main-categories') - -export const fetchChildCategories = (categoryId: string): Promise => axios.get(`/v1/category/${categoryId}/child-categories`) - -export const fetchTrendingCategories = (): Promise => axios.get('/v1/category/trending-categories') diff --git a/client/src/services/api/Tag/index.ts b/client/src/services/api/Tag/index.ts new file mode 100644 index 00000000..0f8e403d --- /dev/null +++ b/client/src/services/api/Tag/index.ts @@ -0,0 +1,4 @@ +import axios, { AxiosResponse } from 'axios' + +export const searchTagByName = (searchValue: string): Promise => axios.get(`/v1/tag/search?searchValue=${searchValue}`) +export const fetchTrendingTags = (): Promise => axios.get('/v1/tag/trending') \ No newline at end of file diff --git a/client/src/services/api/Title/index.ts b/client/src/services/api/Title/index.ts index 5e8dd733..0d4b4ec6 100644 --- a/client/src/services/api/Title/index.ts +++ b/client/src/services/api/Title/index.ts @@ -4,13 +4,13 @@ import axios, { AxiosResponse } from 'axios' export const fetchAllFeeds = ( skip: number, username: string | undefined, - categoryIds: string | undefined, + tags: string | undefined, sortBy?: 'hot' | 'top' | undefined ): Promise => axios.get( '/v1/title/all', { params: { ...username && { author: username }, - ...categoryIds && { categoryIds }, + ...tags && { tags }, ...sortBy && { sortBy }, skip } @@ -79,7 +79,7 @@ export const updateTitle = ( titleId: string, payload: { name: string, - categoryId: string | string[] | null + tags: string[] } ): Promise => axios.patch(`/v1/title/${titleId}`, payload, { headers: { diff --git a/client/src/services/api/index.ts b/client/src/services/api/index.ts index 1189cab9..88f17e7c 100644 --- a/client/src/services/api/index.ts +++ b/client/src/services/api/index.ts @@ -7,7 +7,7 @@ import { API_URL } from '@/../config/constants' axios.defaults.baseURL = API_URL export * from './Auth' -export * from './Category' +export * from './Tag' export * from './Entry' export * from './Message' export * from './Title' diff --git a/client/src/services/initializations/[entry]/index.ts b/client/src/services/initializations/[entry]/index.ts index ed7eab13..02f8cdae 100644 --- a/client/src/services/initializations/[entry]/index.ts +++ b/client/src/services/initializations/[entry]/index.ts @@ -1,11 +1,10 @@ // Local files import { TitleResponseData, EntryResponseData } from '@/@types/api' -import { fetchEntryByEntryId, fetchTitle, fetchOneCategory, getAverageTitleRate } from '@/services/api' +import { fetchEntryByEntryId, fetchTitle, getAverageTitleRate } from '@/services/api' import { EntryPageInitials } from '@/@types/initializations' export const getEntryPageInitialValues = async (entryId: string): Promise => { let title: TitleResponseData - let categoryName: string let averageTitleRate: number let entryData: EntryResponseData @@ -16,10 +15,6 @@ export const getEntryPageInitialValues = async (entryId: string): Promise { title = sRes.data - // Get category - await fetchOneCategory(sRes.data.attributes.category_id).then(({ data }) => { - categoryName = data.attributes.name - }) // Fetch average rate of title await getAverageTitleRate(sRes.data.attributes.id) .then(trRes => averageTitleRate = trRes.data.attributes.rate || 0) @@ -31,7 +26,6 @@ export const getEntryPageInitialValues = async (entryId: string): Promise => { let titleData: TitleResponseData - let categoryData: CategoryResponseData let featuredEntry: string let averageTitleRate: number let entryList: { @@ -17,7 +16,6 @@ export const getFeedPageInitialValues = async (titleSlug: string, entryPage: num let error: boolean = false await fetchTitle(titleSlug, 'slug').then(async titleResponse => { - await fetchOneCategory(titleResponse.data.attributes.category_id).then(({ data }) => categoryData = data) await fetchFeaturedEntryByTitleId(titleResponse.data.attributes.id).then(({ data }) => featuredEntry = data.attributes.text).catch(_error => {}) await getAverageTitleRate(titleResponse.data.attributes.id).then(averageRateResponse => averageTitleRate = averageRateResponse.data.attributes.rate || 0) titleData = titleResponse.data @@ -35,7 +33,6 @@ export const getFeedPageInitialValues = async (titleSlug: string, entryPage: num return { titleData, - categoryData, featuredEntry, averageTitleRate, entryList, diff --git a/client/src/services/initializations/feeds/index.ts b/client/src/services/initializations/feeds/index.ts index f425ea5e..c76356cf 100644 --- a/client/src/services/initializations/feeds/index.ts +++ b/client/src/services/initializations/feeds/index.ts @@ -1,13 +1,13 @@ // Local files -import { fetchTrendingCategories } from '@/services/api' -import { TrendingCategoriesResponseData } from '@/@types/api' +import { fetchTrendingTags } from '@/services/api' +import { TrendingTagsResponseData } from '@/@types/api' import { FeedsPageInitials } from '@/@types/initializations' export const getFeedsPageInitialValues = async (): Promise => { - let trendingCategories: TrendingCategoriesResponseData[] + let trendingTags: TrendingTagsResponseData[] - await fetchTrendingCategories().then(res => trendingCategories = res.data.attributes.categories) + await fetchTrendingTags().then(res => trendingTags = res.data.attributes.tags) .catch((_error) => { }) - return { trendingCategories } + return { trendingTags } } \ No newline at end of file diff --git a/client/src/services/utils.less b/client/src/services/utils.less deleted file mode 100644 index 13ee0df5..00000000 --- a/client/src/services/utils.less +++ /dev/null @@ -1,50 +0,0 @@ -.textOverflow() { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - word-break: break-all; -} - -.textOverflowMulti(@line: 3, @bg: #fff) { - position: relative; - max-height: @line * 1.5em; - margin-right: -1em; - padding-right: 1em; - overflow: hidden; - line-height: 1.5em; - text-align: justify; - &::before { - position: absolute; - right: 14px; - bottom: 0; - padding: 0 1px; - background: @bg; - content: '...'; - } - &::after { - position: absolute; - right: 14px; - width: 1em; - height: 1em; - margin-top: 0.2em; - background: white; - content: ''; - } -} - -// mixins for clearfix -// ------------------------ -.clearfix() { - zoom: 1; - &::before, - &::after { - display: table; - content: ' '; - } - &::after { - clear: both; - height: 0; - font-size: 0; - visibility: hidden; - } -}