diff --git a/apps/nestjs-backend/src/types/i18n.generated.ts b/apps/nestjs-backend/src/types/i18n.generated.ts
index 2192e3f142..23de790817 100644
--- a/apps/nestjs-backend/src/types/i18n.generated.ts
+++ b/apps/nestjs-backend/src/types/i18n.generated.ts
@@ -485,6 +485,8 @@ export type I18nTranslations = {
"addCategory": string;
"selectCategory": string;
};
+ "relatedTemplates": string;
+ "noImage": string;
"baseSelectPanel": {
"title": string;
"description": string;
diff --git a/apps/nextjs-app/src/features/app/components/space/template/RecommendTemplate.tsx b/apps/nextjs-app/src/features/app/components/space/template/RecommendTemplate.tsx
new file mode 100644
index 0000000000..1fbd4373c0
--- /dev/null
+++ b/apps/nextjs-app/src/features/app/components/space/template/RecommendTemplate.tsx
@@ -0,0 +1,93 @@
+import { useQuery } from '@tanstack/react-query';
+import { getPublishedTemplateList } from '@teable/openapi';
+import { ReactQueryKeys } from '@teable/sdk/config/react-query-keys';
+import { Spin } from '@teable/ui-lib/base';
+import { cn } from '@teable/ui-lib/shadcn';
+import { useTranslation } from 'next-i18next';
+import { useMemo } from 'react';
+
+interface IRecommendTemplateProps {
+ filterTemplateIds?: string[];
+ onTemplateClick?: (templateId: string) => void;
+ className?: string;
+}
+
+export const RecommendTemplate = (props: IRecommendTemplateProps) => {
+ const { onTemplateClick, className, filterTemplateIds } = props;
+ const { t } = useTranslation('common');
+
+ const { data: templates, isLoading } = useQuery({
+ queryKey: [...ReactQueryKeys.publishedTemplateList(null, '', true), 'recommend'],
+ queryFn: () => getPublishedTemplateList({ featured: true, take: 4 }).then((res) => res.data),
+ });
+
+ const filteredTemplates = useMemo(() => {
+ return templates?.filter((template) => !filterTemplateIds?.includes(template.id))?.slice(0, 3);
+ }, [templates, filterTemplateIds]);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!templates || templates.length === 0) {
+ return null;
+ }
+
+ const handleTemplateClick = (templateId: string) => {
+ onTemplateClick?.(templateId);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent, templateId: string) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ handleTemplateClick(templateId);
+ }
+ };
+
+ return (
+
+
+ {t('settings.templateAdmin.relatedTemplates')}
+
+
+ {filteredTemplates?.map((template) => (
+
handleTemplateClick(template.id)}
+ onKeyDown={(e) => handleKeyDown(e, template.id)}
+ >
+
+ {template.cover?.presignedUrl ? (
+

+ ) : (
+
+
+ {t('settings.templateAdmin.noImage')}
+
+
+ )}
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/apps/nextjs-app/src/features/app/components/space/template/TemplateDetail.tsx b/apps/nextjs-app/src/features/app/components/space/template/TemplateDetail.tsx
index d1b074b088..ced7a6585e 100644
--- a/apps/nextjs-app/src/features/app/components/space/template/TemplateDetail.tsx
+++ b/apps/nextjs-app/src/features/app/components/space/template/TemplateDetail.tsx
@@ -12,24 +12,29 @@ import { Badge, Button, cn } from '@teable/ui-lib/shadcn';
import { ArrowUpRight, ChevronLeft } from 'lucide-react';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
-import { useMemo } from 'react';
+import { useEffect, useMemo, useRef } from 'react';
import { useSpaceId } from './hooks/use-space-id';
+import { RecommendTemplate } from './RecommendTemplate';
import { TemplatePreview } from './TemplatePreview';
import { TemplatePreviewSheet } from './TemplatePreviewSheet';
interface ITemplateDetailProps {
templateId: string;
onBackToTemplateList?: () => void;
+ onTemplateClick?: (templateId: string) => void;
}
export const TemplateDetail = (props: ITemplateDetailProps) => {
- const { templateId, onBackToTemplateList } = props;
+ const { templateId, onBackToTemplateList, onTemplateClick } = props;
const { t } = useTranslation(['common']);
+ const detailRef = useRef(null);
const isMobile = useIsMobile();
- const { data: templateDetail } = useQuery({
+ const { data: _templateDetail } = useQuery({
queryKey: ReactQueryKeys.templateDetail(templateId),
queryFn: () => getTemplateDetail(templateId).then((res) => res.data),
});
+ const templateDetail = _templateDetail?.id === templateId ? _templateDetail : undefined;
+
const { name, description, categoryId, markdownDescription, cover } = templateDetail || {};
const { data: categoryList } = useQuery({
@@ -71,6 +76,19 @@ export const TemplateDetail = (props: ITemplateDetailProps) => {
},
});
+ const filterTemplateIds = useMemo(() => {
+ return [templateId];
+ }, [templateId]);
+
+ useEffect(() => {
+ if (detailRef.current) {
+ detailRef.current.scrollTo({
+ top: 0,
+ behavior: 'smooth',
+ });
+ }
+ }, [templateId]);
+
if (isMobile) {
return (
@@ -86,7 +104,7 @@ export const TemplateDetail = (props: ITemplateDetailProps) => {
)}
{name}
-
+
{categoryNames.length > 0 && (
{categoryNames.map((categoryName) => (
@@ -131,10 +149,11 @@ export const TemplateDetail = (props: ITemplateDetailProps) => {
{markdownDescription && (
{markdownDescription}
)}
- {/* {!markdownDescription && (
- {t('common:noDescription')}
- )} */}
+
);
@@ -187,16 +206,17 @@ export const TemplateDetail = (props: ITemplateDetailProps) => {
{isLoading && }
-
-
-
+
+
+
{markdownDescription && (
{markdownDescription}
)}
- {/* {!markdownDescription && (
- {t('common:noDescription')}
- )} */}
+
);
diff --git a/apps/nextjs-app/src/features/app/components/space/template/TemplateModal.tsx b/apps/nextjs-app/src/features/app/components/space/template/TemplateModal.tsx
index 782aa11c91..4120db4465 100644
--- a/apps/nextjs-app/src/features/app/components/space/template/TemplateModal.tsx
+++ b/apps/nextjs-app/src/features/app/components/space/template/TemplateModal.tsx
@@ -47,7 +47,14 @@ export const TemplateModal = (props: TemplateModalProps) => {
) : (