diff --git a/src/app/api/tmdb/[...all]/route.ts b/src/app/api/tmdb/[...all]/route.ts
index 0ecbc0ba4a8..39406dfeca3 100644
--- a/src/app/api/tmdb/[...all]/route.ts
+++ b/src/app/api/tmdb/[...all]/route.ts
@@ -26,7 +26,9 @@ export const GET = async (req: NextRequest) => {
const searchString = query.toString()
const url = `https://api.themoviedb.org/3/${pathname.join('/')}${
- searchString ? `?${searchString}` : ''
+ searchString
+ ? `?${searchString}&api_key=${process.env.TMDB_API_KEY}`
+ : `?api_key=${process.env.TMDB_API_KEY}`
}`
const headers = new Headers()
diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx
index 1c259fbcf2a..bf1b32cabd8 100644
--- a/src/app/global-error.tsx
+++ b/src/app/global-error.tsx
@@ -28,7 +28,9 @@ export default function GlobalError({
禁止访问或者 API 服务出现问题
- 重试
+ location.reload()}>
+ 重试
+
diff --git a/src/components/ui/link-card/LinkCard.tsx b/src/components/ui/link-card/LinkCard.tsx
index 1add07014e1..6cd0216cfd6 100644
--- a/src/components/ui/link-card/LinkCard.tsx
+++ b/src/components/ui/link-card/LinkCard.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable unicorn/switch-case-braces */
import { simpleCamelcaseKeys as camelcaseKeys } from '@mx-space/api-client'
import { m, useMotionTemplate, useMotionValue } from 'framer-motion'
import Link from 'next/link'
@@ -11,6 +12,7 @@ import uniqolor from 'uniqolor'
import { LazyLoad } from '~/components/common/Lazyload'
import { MingcuteStarHalfFill } from '~/components/icons/star'
import { usePeek } from '~/components/modules/peek/usePeek'
+import { API_URL } from '~/constants/env'
import { LanguageToColorMap } from '~/constants/language'
import { useIsClientTransition } from '~/hooks/common/use-is-client'
import useIsCommandOrControlPressed from '~/hooks/common/use-is-command-or-control-pressed'
@@ -93,6 +95,7 @@ const LinkCardImpl: FC = (props) => {
[LinkCardSource.GHCommit]: fetchGitHubCommitData,
[LinkCardSource.GHPr]: fetchGitHubPRData,
[LinkCardSource.Self]: fetchMxSpaceData,
+ [LinkCardSource.LEETCODE]: fetchLeetCodeQuestionData,
} as Record
if (tmdbEnabled)
fetchDataFunctions[LinkCardSource.TMDB] = fetchTheMovieDBData
@@ -508,3 +511,113 @@ const fetchTheMovieDBData: FetchObject = {
json.homepage && setFullUrl(json.homepage)
},
}
+
+const fetchLeetCodeQuestionData: FetchObject = {
+ isValid: (id) => {
+ // 检查 titleSlug 是否是一个有效的字符串
+ return typeof id === 'string' && id.length > 0
+ },
+ fetch: async (id, setCardInfo, setFullUrl) => {
+ try {
+ //获取题目信息
+ const body = {
+ query: `query questionData($titleSlug: String!) {\n question(titleSlug: $titleSlug) {translatedTitle\n difficulty\n likes\n topicTags { translatedName\n }\n stats\n }\n}\n`,
+ variables: { titleSlug: id },
+ }
+ const questionData = await fetch(`${API_URL}/fn/leetcode/shiro`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(body),
+ }).then(async (res) => {
+ if (!res.ok) {
+ throw new Error('Failed to fetch LeetCode question title')
+ }
+ return res.json()
+ })
+ const questionTitleData = camelcaseKeys(questionData.data.question)
+ const stats = JSON.parse(questionTitleData.stats)
+ // 设置卡片信息
+ setCardInfo({
+ title: (
+ <>
+
+
+ {questionTitleData.translatedTitle}
+
+
+ {questionTitleData.likes > 0 && (
+
+
+
+ {questionTitleData.likes}
+
+
+ )}
+
+
+ >
+ ),
+ desc: (
+ <>
+
+ {questionTitleData.difficulty}
+
+
+ {questionTitleData.topicTags
+ .map((tag: any) => tag.translatedName)
+ .join(' / ')}
+
+
+ AR: {stats.acRate}
+
+ >
+ ),
+ image:
+ 'https://upload.wikimedia.org/wikipedia/commons/1/19/LeetCode_logo_black.png',
+ color: getDifficultyColor(questionTitleData.difficulty),
+ })
+
+ setFullUrl(`https://leetcode.cn/problems/${id}/description`)
+ } catch (err) {
+ console.error('Error fetching LeetCode question data:', err)
+ throw err
+ }
+ },
+}
+
+// 映射难度到颜色的函数
+function getDifficultyColor(difficulty: string) {
+ switch (difficulty) {
+ case 'Easy':
+ return '#00BFA5'
+ case 'Medium':
+ return '#FFA726'
+ case 'Hard':
+ return '#F44336'
+ default:
+ return '#757575'
+ }
+}
+
+// 难度字体颜色className
+function getDifficultyColorClass(difficulty: string) {
+ switch (difficulty) {
+ case 'Easy':
+ return 'text-green-500'
+ case 'Medium':
+ return 'text-yellow-500'
+ case 'Hard':
+ return 'text-red-500'
+ default:
+ return 'text-gray-500'
+ }
+}
+
+interface LeetCodeResponse {
+ query: string
+ variables: Record
+}
diff --git a/src/components/ui/link-card/enums.tsx b/src/components/ui/link-card/enums.tsx
index 52ea50bee6d..041116b8ef4 100644
--- a/src/components/ui/link-card/enums.tsx
+++ b/src/components/ui/link-card/enums.tsx
@@ -7,4 +7,5 @@ export enum LinkCardSource {
GHCommit = 'gh-commit',
GHPr = 'gh-pr',
TMDB = 'tmdb',
+ LEETCODE = 'leetcode',
}
diff --git a/src/components/ui/markdown/renderers/LinkRenderer.tsx b/src/components/ui/markdown/renderers/LinkRenderer.tsx
index 37803f55db1..365215c8dbc 100644
--- a/src/components/ui/markdown/renderers/LinkRenderer.tsx
+++ b/src/components/ui/markdown/renderers/LinkRenderer.tsx
@@ -18,6 +18,7 @@ import {
isGithubPrUrl,
isGithubRepoUrl,
isGithubUrl,
+ isLeetCodeUrl,
isSelfArticleUrl,
isSelfThinkingUrl,
isTMDBUrl,
@@ -144,6 +145,17 @@ export const BlockLinkRenderer = ({
return fallbackElement
}
+
+ case isLeetCodeUrl(url): {
+ return (
+
+ )
+ }
+
case isBilibiliVideoUrl(url): {
const { id } = parseBilibiliVideoUrl(url)
diff --git a/src/lib/link-parser.ts b/src/lib/link-parser.ts
index 5e45c4d15aa..c2d9201cd34 100644
--- a/src/lib/link-parser.ts
+++ b/src/lib/link-parser.ts
@@ -5,7 +5,9 @@ import { isClientSide, isDev } from './env'
export const getTweetId = (url: URL) => url.pathname.split('/').pop()!
const GITHUB_HOST = 'github.com'
-
+export const isLeetCodeUrl = (url: URL) => {
+ return url.hostname === 'leetcode.cn' || url.hostname === 'leetcode.com'
+}
export const isGithubRepoUrl = (url: URL) =>
url.hostname === GITHUB_HOST &&
url.pathname.startsWith('/') &&