Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(in-app): preset article data #824

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 9 additions & 42 deletions in-app/v1/src/App/Articles/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { useMutation } from 'react-query';
import { useTranslation } from 'react-i18next';
import { Route, Routes, useParams, useSearchParams, Link } from 'react-router-dom';
import { useParams, useSearchParams, Link } from 'react-router-dom';
import { http } from '@/leancloud';
import { PageContent, PageHeader } from '@/components/Page';
import { QueryWrapper } from '@/components/QueryWrapper';
Expand All @@ -11,36 +11,16 @@ import CheckIcon from '@/icons/Check';
import ThumbDownIcon from '@/icons/ThumbDown';
import ThumbUpIcon from '@/icons/ThumbUp';
import { useAuth } from '@/states/auth';
import { ArticleListItem } from './utils';
import { Helmet } from 'react-helmet-async';
import { format } from 'date-fns';
import { zhCN } from 'date-fns/locale';
import { useFAQs } from '@/api/category';
import { useArticle } from '@/api/article';

function RelatedFAQs({ categoryId, articleId }: { categoryId: string; articleId: string }) {
const { t } = useTranslation();
const { data: FAQs, isLoading: FAQsIsLoading, isSuccess: FAQsIsReady } = useFAQs(categoryId);
if (!FAQs) {
return null;
}
const relatedFAQs = FAQs.filter((faq) => faq.id !== articleId);
if (relatedFAQs.length === 0) {
return null;
}
return (
<div className="pt-6 pb-2">
<h2 className="px-4 py-3 font-bold">{t('faqs.similar')}</h2>
{relatedFAQs.map((FAQ) => (
<ArticleListItem article={FAQ} fromCategory={categoryId} key={FAQ.id} />
))}
</div>
);
}
enum FeedbackType {
Upvote = 1,
Downvote = -1,
}

async function feedback(articleId: string, type: FeedbackType) {
return await http.post(`/api/2/articles/${articleId}/feedback`, {
type,
Expand Down Expand Up @@ -89,16 +69,18 @@ function Feedback({ articleId }: { articleId: string }) {
);
}

function ArticleDetail() {
const [t, i18n] = useTranslation();
export function ArticleDetail() {
const { t } = useTranslation();
const { id } = useParams();

const [search] = useSearchParams();
const categoryId = search.get('from-category');
const isNotice = !!search.get('from-notice');

const articleId = id?.split('-').shift();
const result = useArticle(articleId!);
const articleId = id!.split('-').shift();
const result = useArticle(articleId || id!, {
staleTime: Infinity,
cacheTime: Infinity,
});
const { data: article } = result;

const { user } = useAuth();
Expand Down Expand Up @@ -143,22 +125,7 @@ function ArticleDetail() {
</Link>
</div>
)}
{/* {categoryId && auth && (
<p className="my-6 px-4 text-center">
<span className="block mb-2 text-sm">若以上内容没有帮助到你</span>
<NewTicketButton categoryId={categoryId} />
</p>
)} */}
{/* {categoryId && id && <RelatedFAQs categoryId={categoryId} articleId={id} />} */}
</PageContent>
</QueryWrapper>
);
}

export default function Articles() {
return (
<Routes>
<Route path=":id" element={<ArticleDetail />} />
</Routes>
);
}
18 changes: 7 additions & 11 deletions in-app/v1/src/App/Articles/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import { Link } from 'react-router-dom';
import { Link, LinkProps } from 'react-router-dom';
import classNames from 'classnames';

import { Article } from '@/types';
import { ListItem } from '../Categories';

export const NoticeLink = ({
article,
className,
children = article.title,
}: {
interface NoticeLinkProps extends Omit<LinkProps, 'to'> {
article: Article;
className?: string;
children?: React.ReactNode;
}) => {
}

export const NoticeLink = ({ article, children, ...props }: NoticeLinkProps) => {
return (
<Link to={`/articles/${article.slug}?from-notice=true`} className={className}>
{children}
<Link {...props} to={`/articles/${article.slug}?from-notice=true`}>
{children ?? article.title}
</Link>
);
};
Expand Down
6 changes: 4 additions & 2 deletions in-app/v1/src/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import Home from './Home';
import Categories from './Categories';
import Tickets from './Tickets';
import NotFound from './NotFound';
import Articles from './Articles';
import { ArticleDetail } from './Articles';
import TopCategories from './TopCategories';
import Test from './Test';

Expand Down Expand Up @@ -173,7 +173,9 @@ const AppRoutes = () => {
}
/>
<Route path="/categories" element={<TopCategories />} />
<Route path="/articles/*" element={<Articles />} />
<Route path="/articles">
<Route path=":id" element={<ArticleDetail />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
);
Expand Down
20 changes: 16 additions & 4 deletions in-app/v1/src/api/article.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import { useEffect } from 'react';
import { useQuery, useQueryClient, UseQueryOptions } from 'react-query';
import { http } from '@/leancloud';
import { Article } from '@/types';
import { useQuery } from 'react-query';

async function getArticle(id: string, locale?: string) {
return (await http.get<Article>(`/api/2/articles/${id}`, { params: { locale } })).data;
const res = await http.get<Article>(`/api/2/articles/${id}`, { params: { locale } });
return res.data;
}

export function useArticle(id: string) {
export function useArticle(id: string, options?: UseQueryOptions<Article>) {
return useQuery({
queryKey: ['article', id],
queryFn: () => getArticle(id),
staleTime: 60_000,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个 stale time 是不是不要完全去掉会更好。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

在 ArticleDetail 里设置成 Infinity 了。

...options,
});
}

export function usePresetArticles(articles?: Article[]) {
const queryClient = useQueryClient();

useEffect(() => {
articles?.forEach((article) => {
queryClient.setQueryData(['article', article.id], article);
});
}, [articles]);
}
30 changes: 27 additions & 3 deletions in-app/v1/src/api/category.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { useMemo } from 'react';
import { UseQueryOptions, useQuery } from 'react-query';
import { uniqBy } from 'lodash-es';

import { useRootCategory } from '@/states/root-category';
import { http } from '@/leancloud';
import { Article } from '@/types';
import { usePresetArticles } from './article';

export interface Category {
id: string;
Expand Down Expand Up @@ -64,12 +68,24 @@ export function useCategory(id: string, options?: UseQueryOptions<Category>) {

export function useCategoryTopics(options?: UseQueryOptions<CategoryTopics[]>) {
const rootCategory = useRootCategory();
return useQuery({

const result = useQuery({
queryKey: ['categoryTopic', rootCategory.id],
queryFn: () => fetchCategoryTopic(rootCategory.id),
staleTime: Infinity,
...options,
});

usePresetArticles(
useMemo(() => {
if (result.data) {
const articles = result.data.flatMap((topic) => topic.articles);
return uniqBy(articles, (article) => article.id);
}
}, [result.data])
);

return result;
}

async function fetchFAQs(categoryId?: string, locale?: string): Promise<Article[]> {
Expand All @@ -83,11 +99,15 @@ async function fetchFAQs(categoryId?: string, locale?: string): Promise<Article[
}

export function useFAQs(categoryId?: string) {
return useQuery({
const result = useQuery({
queryKey: ['category-faqs', categoryId],
queryFn: () => fetchFAQs(categoryId),
staleTime: 1000 * 60,
});

usePresetArticles(result.data);

return result;
}

async function fetchNotices(categoryId?: string, locale?: string): Promise<Article[]> {
Expand All @@ -101,9 +121,13 @@ async function fetchNotices(categoryId?: string, locale?: string): Promise<Artic
}

export function useNotices(categoryId?: string) {
return useQuery({
const result = useQuery({
queryKey: ['category-notices', categoryId],
queryFn: () => fetchNotices(categoryId),
staleTime: 1000 * 60,
});

usePresetArticles(result.data);

return result;
}
6 changes: 3 additions & 3 deletions next/api/src/controller/category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { auth, customerServiceOnly } from '@/middleware';
import { Category } from '@/model/Category';
import { TicketForm } from '@/model/TicketForm';
import { User } from '@/model/User';
import { ArticleTranslationAbstractResponse } from '@/response/article';
import { ArticleTranslationResponse } from '@/response/article';
import { CategoryResponse } from '@/response/category';
import { TicketFieldVariantResponse } from '@/response/ticket-field';
import { ArticleTopicFullResponse } from '@/response/article-topic';
Expand Down Expand Up @@ -159,7 +159,7 @@ export class CategoryController {
}

@Get(':id/faqs')
@ResponseBody(ArticleTranslationAbstractResponse)
@ResponseBody(ArticleTranslationResponse)
async getFAQs(@Param('id', FindCategoryPipe) category: Category, @Locales() locales: ILocale) {
if (!category.FAQIds) {
return [];
Expand All @@ -176,7 +176,7 @@ export class CategoryController {
}

@Get(':id/notices')
@ResponseBody(ArticleTranslationAbstractResponse)
@ResponseBody(ArticleTranslationResponse)
async getNotices(@Param('id', FindCategoryPipe) category: Category, @Locales() locales: ILocale) {
if (!category.noticeIds) {
return [];
Expand Down
1 change: 0 additions & 1 deletion next/api/src/model/ArticleTranslation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ export const getPublicTranslations = mem(
(articleId: string) =>
ArticleTranslation.queryBuilder()
.where('article', '==', Article.ptr(articleId))
// .where('article.private', '==', false)
.where('private', '==', false)
.preload('article')
.preload('revision')
Expand Down
4 changes: 2 additions & 2 deletions next/api/src/response/article-topic.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ArticleTopic } from '@/model/ArticleTopic';
import { ArticleTranslationAbstractResponse } from './article';
import { ArticleTranslationResponse } from './article';

export class ArticleTopicResponse {
constructor(readonly topic: ArticleTopic) {}
Expand All @@ -25,7 +25,7 @@ export class ArticleTopicFullResponse extends ArticleTopicResponse {
return {
...super.toJSON(),
articles: this.topic.translations?.map(
(translation) => new ArticleTranslationAbstractResponse(translation)
(translation) => new ArticleTranslationResponse(translation)
),
};
}
Expand Down