app-tutor-ai-consumer 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/config/vitest/setupTests.ts +6 -1
- package/package.json +11 -2
- package/public/assets/images/default-image.png +0 -0
- package/src/@types/declarations.d.ts +12 -3
- package/src/config/dayjs/index.ts +2 -0
- package/src/config/dayjs/init.ts +28 -0
- package/src/config/dayjs/utils/format-fulldate.ts +7 -0
- package/src/config/dayjs/utils/format-time.ts +20 -0
- package/src/config/dayjs/utils/index.ts +2 -0
- package/src/config/styles/global.css +19 -1
- package/src/config/tanstack/query-provider.tsx +1 -1
- package/src/config/tests/handlers.ts +6 -0
- package/src/index.tsx +4 -0
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/markdownrenderer/__tests__/markdown.stub.ts +334 -0
- package/src/lib/components/markdownrenderer/components/index.ts +1 -0
- package/src/lib/components/markdownrenderer/components/md-code-block/index.ts +1 -0
- package/src/lib/components/markdownrenderer/components/md-code-block/md-code-block.tsx +71 -0
- package/src/lib/components/markdownrenderer/index.ts +2 -0
- package/src/lib/components/markdownrenderer/markdownrenderer.tsx +115 -0
- package/src/lib/utils/constants.ts +1 -1
- package/src/lib/utils/copy-text-to-clipboard.tsx +13 -0
- package/src/lib/utils/extract-text-from-react-nodes.ts +23 -0
- package/src/lib/utils/index.ts +3 -0
- package/src/lib/utils/urls.ts +20 -0
- package/src/modules/messages/__tests__/imessage-with-sender-data.builder.ts +113 -0
- package/src/modules/messages/__tests__/imessage-with-sender-data.mock.ts +15 -0
- package/src/modules/messages/components/index.ts +2 -0
- package/src/modules/messages/components/message-img/index.ts +1 -0
- package/src/modules/messages/components/message-img/message-img.tsx +47 -0
- package/src/modules/messages/components/message-item/index.ts +2 -0
- package/src/modules/messages/components/message-item/message-item.spec.tsx +26 -0
- package/src/modules/messages/components/message-item/message-item.tsx +15 -0
- package/src/modules/messages/components/messages-list/messages-list.tsx +46 -17
- package/src/modules/messages/hooks/index.ts +1 -0
- package/src/modules/messages/hooks/use-fetch-messages/index.ts +2 -0
- package/src/modules/messages/hooks/use-fetch-messages/use-fetch-messages.spec.tsx +46 -0
- package/src/modules/messages/hooks/use-fetch-messages/use-fetch-messages.tsx +103 -0
- package/src/modules/messages/service.ts +26 -3
- package/src/modules/messages/types.ts +33 -2
- package/src/modules/messages/utils/index.ts +1 -0
- package/src/modules/messages/utils/messages-parser/index.ts +1 -0
- package/src/modules/messages/utils/messages-parser/utils.ts +28 -0
- package/src/modules/profile/__tests__/profile-api-props.builder.ts +74 -0
- package/src/modules/profile/__tests__/profile-props.builder.ts +42 -0
- package/src/modules/profile/constants.ts +3 -0
- package/src/modules/profile/hooks/index.ts +1 -0
- package/src/modules/profile/hooks/use-get-profile/index.ts +1 -0
- package/src/modules/profile/hooks/use-get-profile/use-get-profile.spec.tsx +20 -0
- package/src/modules/profile/hooks/use-get-profile/use-get-profile.tsx +14 -0
- package/src/modules/profile/index.ts +4 -0
- package/src/modules/profile/service.tsx +19 -0
- package/src/modules/profile/types.ts +17 -0
- package/src/modules/widget/components/chat-page/chat-page.tsx +3 -3
- package/src/modules/widget/components/container/container.tsx +1 -1
- package/src/modules/widget/components/onboarding-page/onboarding-page.tsx +16 -14
- package/src/modules/widget/components/starter-page/starter-page.tsx +3 -3
- package/src/modules/widget/store/widget-settings.atom.ts +3 -1
- package/src/config/styles/shared-styles.module.css +0 -16
- package/src/modules/widget/components/container/styles.module.css +0 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
# [1.4.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.3.0...v1.4.0) (2025-06-26)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
- pr issues ([74efaec](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/74efaecaec56aa84808325cb05bc50bd0b50a6ef))
|
|
6
|
+
- pr issues ([ab7a3a8](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/ab7a3a80a5d45b857effb2d281d9523d9789af98))
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- add tutor message markdown support ([4328fc4](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/4328fc402c47f501ddd9761f612325515fc8c6e2))
|
|
11
|
+
- add tutor messages and profile service ([213611b](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/213611b570a163c27d980a83a8a8a4dd2cf6e077))
|
|
12
|
+
|
|
1
13
|
# [1.3.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.2.0...v1.3.0) (2025-06-24)
|
|
2
14
|
|
|
3
15
|
### Bug Fixes
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
+
import 'dayjs/locale/pt-br'
|
|
1
2
|
import '@testing-library/jest-dom/vitest'
|
|
2
3
|
|
|
3
4
|
import { cleanup, configure } from '@testing-library/react'
|
|
4
5
|
|
|
5
6
|
import { serviceWorker } from '@/src/config/tests/worker'
|
|
7
|
+
import { initDayjs } from '@/src/config/dayjs'
|
|
8
|
+
import * as I18n from '@/src/config/i18n'
|
|
6
9
|
|
|
7
10
|
configure({ testIdAttribute: 'data-test' })
|
|
8
11
|
|
|
9
12
|
beforeEach(() => {
|
|
10
13
|
cleanup()
|
|
14
|
+
vi.spyOn(I18n, 't').mockImplementation((...args) => args[0])
|
|
11
15
|
})
|
|
12
16
|
|
|
13
17
|
// Start worker before all tests
|
|
14
|
-
beforeAll(() => {
|
|
18
|
+
beforeAll(async () => {
|
|
15
19
|
serviceWorker.listen()
|
|
20
|
+
await initDayjs('pt-br')
|
|
16
21
|
})
|
|
17
22
|
|
|
18
23
|
// Close serviceWorker after all tests
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "app-tutor-ai-consumer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"@testing-library/user-event": "~14.6.1",
|
|
55
55
|
"@types/axios": "~0.9.36",
|
|
56
56
|
"@types/chance": "~1.1.6",
|
|
57
|
+
"@types/linkify-it": "~5.0.0",
|
|
57
58
|
"@types/node": "~24.0.0",
|
|
58
59
|
"@types/react": "~19.1.7",
|
|
59
60
|
"@types/react-dom": "~19.1.6",
|
|
@@ -107,12 +108,20 @@
|
|
|
107
108
|
"@tanstack/react-query": "~5.80.6",
|
|
108
109
|
"@tanstack/react-query-persist-client": "~5.80.7",
|
|
109
110
|
"clsx": "~2.1.1",
|
|
111
|
+
"dayjs": "~1.11.13",
|
|
110
112
|
"i18next": "~25.2.1",
|
|
111
113
|
"i18next-resources-to-backend": "~1.2.1",
|
|
112
114
|
"jotai": "~2.12.5",
|
|
115
|
+
"linkify-it": "~5.0.0",
|
|
116
|
+
"prism-react-renderer": "~2.4.1",
|
|
113
117
|
"react": "~19.1.0",
|
|
114
118
|
"react-dom": "~19.1.0",
|
|
115
|
-
"react-i18next": "~15.5.2"
|
|
119
|
+
"react-i18next": "~15.5.2",
|
|
120
|
+
"react-markdown": "~10.1.0",
|
|
121
|
+
"rehype-raw": "~7.0.0",
|
|
122
|
+
"rehype-sanitize": "~6.0.0",
|
|
123
|
+
"remark-breaks": "~4.0.0",
|
|
124
|
+
"remark-gfm": "~4.0.1"
|
|
116
125
|
},
|
|
117
126
|
"optionalDependencies": {
|
|
118
127
|
"@rollup/rollup-linux-x64-gnu": "4.6.1",
|
|
Binary file
|
|
@@ -7,11 +7,20 @@ declare module '*.css'
|
|
|
7
7
|
|
|
8
8
|
declare module '*.css?inline'
|
|
9
9
|
|
|
10
|
-
declare module '*.png'
|
|
10
|
+
declare module '*.png' {
|
|
11
|
+
const content: string
|
|
12
|
+
export default content
|
|
13
|
+
}
|
|
11
14
|
|
|
12
|
-
declare module '*.jpg'
|
|
15
|
+
declare module '*.jpg' {
|
|
16
|
+
const content: string
|
|
17
|
+
export default content
|
|
18
|
+
}
|
|
13
19
|
|
|
14
|
-
declare module '*.gif'
|
|
20
|
+
declare module '*.gif' {
|
|
21
|
+
const content: string
|
|
22
|
+
export default content
|
|
23
|
+
}
|
|
15
24
|
|
|
16
25
|
declare module '*.svg?url' {
|
|
17
26
|
const content: string
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import dayjs from 'dayjs'
|
|
2
|
+
import calendar from 'dayjs/plugin/calendar'
|
|
3
|
+
import isBetween from 'dayjs/plugin/isBetween'
|
|
4
|
+
import isToday from 'dayjs/plugin/isToday'
|
|
5
|
+
import localizedFormat from 'dayjs/plugin/localizedFormat'
|
|
6
|
+
import timezone from 'dayjs/plugin/timezone'
|
|
7
|
+
import utc from 'dayjs/plugin/utc'
|
|
8
|
+
|
|
9
|
+
import { DEFAULT_LANGUAGE } from '../i18n'
|
|
10
|
+
|
|
11
|
+
export const initDayjs = async (locale: string = DEFAULT_LANGUAGE) => {
|
|
12
|
+
dayjs.extend(localizedFormat)
|
|
13
|
+
dayjs.extend(utc)
|
|
14
|
+
dayjs.extend(timezone)
|
|
15
|
+
dayjs.extend(isBetween)
|
|
16
|
+
dayjs.extend(isToday)
|
|
17
|
+
dayjs.extend(calendar)
|
|
18
|
+
|
|
19
|
+
const lng = locale.match(/^pt/i) ? 'pt-br' : locale
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await import(`dayjs/locale/${lng}`)
|
|
23
|
+
dayjs.locale(lng)
|
|
24
|
+
} catch {
|
|
25
|
+
await import(`dayjs/locale/${DEFAULT_LANGUAGE}`)
|
|
26
|
+
dayjs.locale(DEFAULT_LANGUAGE)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import dayjs from 'dayjs'
|
|
2
|
+
|
|
3
|
+
import { t } from '@/src/config/i18n'
|
|
4
|
+
|
|
5
|
+
export const formatTime = (timestamp: dayjs.ConfigType, ignoreTime = false) => {
|
|
6
|
+
const time = dayjs(timestamp)
|
|
7
|
+
|
|
8
|
+
if (!time.isValid()) {
|
|
9
|
+
return 'Invalid date'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const daysDiff = dayjs().diff(time, 'day')
|
|
13
|
+
|
|
14
|
+
return time.calendar(null, {
|
|
15
|
+
sameDay: ignoreTime ? `[${t('general.today')}]` : 'LT',
|
|
16
|
+
lastDay: `[${t('general.yesterday')}]`,
|
|
17
|
+
lastWeek: daysDiff <= 7 ? 'dddd' : 'L',
|
|
18
|
+
sameElse: 'L'
|
|
19
|
+
})
|
|
20
|
+
}
|
|
@@ -77,10 +77,28 @@
|
|
|
77
77
|
--ai-color-secondary: #6ba1f0;
|
|
78
78
|
--ai-color-dark: #111925;
|
|
79
79
|
--ai-color-chat-response: #26202f;
|
|
80
|
+
|
|
81
|
+
/* Size */
|
|
82
|
+
--hc-size-spacing-2: 0.5rem;
|
|
83
|
+
--hc-size-border-medium: 0.5rem;
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
#hotmart-app-tutor-ai-consumer-root {
|
|
83
|
-
|
|
87
|
+
& *::-webkit-scrollbar {
|
|
88
|
+
width: var(--hc-size-spacing-2);
|
|
89
|
+
height: var(--hc-size-spacing-2);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
& *::-webkit-scrollbar-track {
|
|
93
|
+
background: transparent;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
& *::-webkit-scrollbar-thumb {
|
|
97
|
+
background: var(--hc-color-neutral-800);
|
|
98
|
+
border-radius: var(--hc-size-border-medium);
|
|
99
|
+
border: calc(var(--hc-size-border-medium) / 2) solid transparent;
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
font-family:
|
|
85
103
|
'Nunito Sans',
|
|
86
104
|
-apple-system,
|
|
@@ -10,7 +10,7 @@ function QueryProvider({ children, showDevTools = true }: QueryProviderProps) {
|
|
|
10
10
|
return (
|
|
11
11
|
<PersistQueryClientProvider client={queryClient} persistOptions={{ persister }}>
|
|
12
12
|
{children}
|
|
13
|
-
{showDevTools && <ReactQueryDevtools />}
|
|
13
|
+
{showDevTools && <ReactQueryDevtools buttonPosition='top-right' />}
|
|
14
14
|
</PersistQueryClientProvider>
|
|
15
15
|
)
|
|
16
16
|
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { http, HttpResponse } from 'msw'
|
|
2
2
|
|
|
3
|
+
import { ProfileEndpoints } from '@/src/modules/profile'
|
|
4
|
+
import ProfileAPIPropsBuilder from '@/src/modules/profile/__tests__/profile-api-props.builder'
|
|
5
|
+
|
|
3
6
|
export const handlers = [
|
|
4
7
|
http.all('https://tracking-api.buildstaging.com/rest/track/event/json/sync', () => {
|
|
5
8
|
return HttpResponse.json({ ok: true })
|
|
6
9
|
}),
|
|
7
10
|
http.all('https://c3po-api-auth.buildstaging.com/v1/auth/sparkie', () => {
|
|
8
11
|
return HttpResponse.json({ ok: true })
|
|
12
|
+
}),
|
|
13
|
+
http.all(ProfileEndpoints.getProfile(), () => {
|
|
14
|
+
return HttpResponse.json(new ProfileAPIPropsBuilder())
|
|
9
15
|
})
|
|
10
16
|
]
|
package/src/index.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { StrictMode } from 'react'
|
|
2
2
|
import { createRoot } from 'react-dom/client'
|
|
3
3
|
|
|
4
|
+
import { initDayjs } from './config/dayjs'
|
|
4
5
|
import { initLanguage } from './config/i18n'
|
|
6
|
+
import { initAxios } from './config/request/api'
|
|
5
7
|
import { Main } from './main'
|
|
6
8
|
import { TutorWidgetEvents, TutorWidgetEventTypes } from './modules/widget'
|
|
7
9
|
import type { StartTutorWidgetProps } from './types'
|
|
@@ -13,7 +15,9 @@ window.startTutorWidget = async ({
|
|
|
13
15
|
const rootElement = document.getElementById(elementId) as HTMLElement
|
|
14
16
|
const root = createRoot(rootElement)
|
|
15
17
|
|
|
18
|
+
initAxios(settings.hotmartToken)
|
|
16
19
|
await initLanguage(settings.locale)
|
|
20
|
+
await initDayjs(settings.locale)
|
|
17
21
|
|
|
18
22
|
if (root)
|
|
19
23
|
root.render(
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
export const TEST_MARKDOWN_STUB = `# Main Heading - Testing Markdown Renderer
|
|
2
|
+
|
|
3
|
+
This is a comprehensive test for our markdown renderer component. Let's see how it handles various elements.
|
|
4
|
+
|
|
5
|
+
## Subheading Level 2
|
|
6
|
+
|
|
7
|
+
### Subheading Level 3
|
|
8
|
+
|
|
9
|
+
#### Subheading Level 4
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Text Formatting
|
|
14
|
+
|
|
15
|
+
Here's some **bold text** and *italic text*. You can also combine them for ***bold and italic***.
|
|
16
|
+
|
|
17
|
+
We also support ~~strikethrough text~~ and \`inline code snippets\`.
|
|
18
|
+
|
|
19
|
+
> This is a blockquote. It should stand out from regular text.
|
|
20
|
+
>
|
|
21
|
+
> Blockquotes can span multiple lines and even contain other markdown elements like **bold text**.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Links
|
|
26
|
+
|
|
27
|
+
- [External Link to React Documentation](https://reactjs.org/docs/getting-started.html)
|
|
28
|
+
- [Internal Link](#code-examples)
|
|
29
|
+
- [Email Link](mailto:test@example.com)
|
|
30
|
+
- Auto-link: https://github.com
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Images
|
|
35
|
+
|
|
36
|
+
### Regular Image
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
### Image with Alt Text
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
### Broken Image (for error handling)
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Lists
|
|
48
|
+
|
|
49
|
+
### Unordered List
|
|
50
|
+
- First item
|
|
51
|
+
- Second item with **bold text**
|
|
52
|
+
- Third item with [a link](https://example.com)
|
|
53
|
+
- Nested item 1
|
|
54
|
+
- Nested item 2
|
|
55
|
+
- Double nested item
|
|
56
|
+
- Fourth item with \`inline code\`
|
|
57
|
+
|
|
58
|
+
### Ordered List
|
|
59
|
+
1. First numbered item
|
|
60
|
+
2. Second numbered item
|
|
61
|
+
3. Third item with multiple lines
|
|
62
|
+
|
|
63
|
+
This paragraph is part of item 3.
|
|
64
|
+
|
|
65
|
+
4. Fourth item
|
|
66
|
+
1. Nested numbered item
|
|
67
|
+
2. Another nested item
|
|
68
|
+
|
|
69
|
+
### Task List (GitHub Flavored Markdown)
|
|
70
|
+
- [x] Completed task
|
|
71
|
+
- [ ] Incomplete task
|
|
72
|
+
- [x] Another completed task
|
|
73
|
+
- [ ] Task with **bold text**
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Code Examples
|
|
78
|
+
|
|
79
|
+
### Inline Code
|
|
80
|
+
Use the \`useState\` hook for state management in React.
|
|
81
|
+
|
|
82
|
+
### JavaScript Code Block
|
|
83
|
+
\`\`\`javascript
|
|
84
|
+
function greetUser(name) {
|
|
85
|
+
if (!name) {
|
|
86
|
+
return "Hello, World!";
|
|
87
|
+
}
|
|
88
|
+
return \`Hello, \${name}!\`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const user = "John Doe";
|
|
92
|
+
console.log(greetUser(user));
|
|
93
|
+
\`\`\`
|
|
94
|
+
|
|
95
|
+
### TypeScript React Component
|
|
96
|
+
\`\`\`tsx
|
|
97
|
+
import React, { useState, useEffect } from 'react';
|
|
98
|
+
|
|
99
|
+
interface UserProps {
|
|
100
|
+
id: number;
|
|
101
|
+
name: string;
|
|
102
|
+
email?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const UserComponent: React.FC<UserProps> = ({ id, name, email }) => {
|
|
106
|
+
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
console.log(\`User \${name} loaded\`);
|
|
110
|
+
}, [name]);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className="user-card">
|
|
114
|
+
<h2>{name}</h2>
|
|
115
|
+
{email && <p>Email: {email}</p>}
|
|
116
|
+
<button onClick={() => setIsLoading(!isLoading)}>
|
|
117
|
+
{isLoading ? 'Loading...' : 'Load Data'}
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export default UserComponent;
|
|
124
|
+
\`\`\`
|
|
125
|
+
|
|
126
|
+
### CSS Code Block
|
|
127
|
+
\`\`\`css
|
|
128
|
+
.markdown-container {
|
|
129
|
+
max-width: 800px;
|
|
130
|
+
margin: 0 auto;
|
|
131
|
+
padding: 2rem;
|
|
132
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.code-block {
|
|
136
|
+
background-color: #f6f8fa;
|
|
137
|
+
border-radius: 6px;
|
|
138
|
+
padding: 16px;
|
|
139
|
+
overflow-x: auto;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@media (max-width: 768px) {
|
|
143
|
+
.markdown-container {
|
|
144
|
+
padding: 1rem;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
\`\`\`
|
|
148
|
+
|
|
149
|
+
### JSON Data
|
|
150
|
+
\`\`\`json
|
|
151
|
+
{
|
|
152
|
+
"name": "my-react-app",
|
|
153
|
+
"version": "1.0.0",
|
|
154
|
+
"dependencies": {
|
|
155
|
+
"react": "^18.2.0",
|
|
156
|
+
"react-dom": "^18.2.0",
|
|
157
|
+
"react-markdown": "^8.0.7"
|
|
158
|
+
},
|
|
159
|
+
"scripts": {
|
|
160
|
+
"dev": "vite",
|
|
161
|
+
"build": "vite build",
|
|
162
|
+
"test": "vitest"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
\`\`\`
|
|
166
|
+
|
|
167
|
+
### Bash Commands
|
|
168
|
+
\`\`\`bash
|
|
169
|
+
# Install dependencies
|
|
170
|
+
npm install react-markdown
|
|
171
|
+
|
|
172
|
+
# Start development server
|
|
173
|
+
npm run dev
|
|
174
|
+
|
|
175
|
+
# Build for production
|
|
176
|
+
npm run build
|
|
177
|
+
|
|
178
|
+
# Run tests
|
|
179
|
+
npm test
|
|
180
|
+
\`\`\`
|
|
181
|
+
|
|
182
|
+
### Python Code
|
|
183
|
+
\`\`\`python
|
|
184
|
+
def fibonacci(n):
|
|
185
|
+
"""Generate Fibonacci sequence up to n terms."""
|
|
186
|
+
if n <= 0:
|
|
187
|
+
return []
|
|
188
|
+
elif n == 1:
|
|
189
|
+
return [0]
|
|
190
|
+
elif n == 2:
|
|
191
|
+
return [0, 1]
|
|
192
|
+
|
|
193
|
+
sequence = [0, 1]
|
|
194
|
+
for i in range(2, n):
|
|
195
|
+
sequence.append(sequence[i-1] + sequence[i-2])
|
|
196
|
+
|
|
197
|
+
return sequence
|
|
198
|
+
|
|
199
|
+
# Example usage
|
|
200
|
+
print(fibonacci(10))
|
|
201
|
+
\`\`\`
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Tables
|
|
206
|
+
|
|
207
|
+
### Simple Table
|
|
208
|
+
| Name | Age | City |
|
|
209
|
+
|------|-----|------|
|
|
210
|
+
| John | 25 | New York |
|
|
211
|
+
| Jane | 30 | Los Angeles |
|
|
212
|
+
| Bob | 35 | Chicago |
|
|
213
|
+
|
|
214
|
+
### Complex Table with Alignment
|
|
215
|
+
| Feature | React | Vue | Angular |
|
|
216
|
+
|:--------|:-----:|:---:|--------:|
|
|
217
|
+
| Learning Curve | Easy | Easy | Steep |
|
|
218
|
+
| Performance | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
|
|
219
|
+
| Community | Large | Medium | Large |
|
|
220
|
+
| Bundle Size | Small | Small | Large |
|
|
221
|
+
|
|
222
|
+
### Table with Code and Links
|
|
223
|
+
| Language | Extension | Documentation |
|
|
224
|
+
|----------|-----------|---------------|
|
|
225
|
+
| JavaScript | \`.js\` | [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript) |
|
|
226
|
+
| TypeScript | \`.ts\` | [TS Docs](https://www.typescriptlang.org/docs/) |
|
|
227
|
+
| Python | \`.py\` | [Python Docs](https://docs.python.org/3/) |
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Special Characters and Escaping
|
|
232
|
+
|
|
233
|
+
Here are some special characters: & < > " '
|
|
234
|
+
|
|
235
|
+
HTML entities: & < > " '
|
|
236
|
+
|
|
237
|
+
Markdown escaping: \\* \\_ \\# \\[ \\] \\( \\)
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Horizontal Rules
|
|
242
|
+
|
|
243
|
+
Above this line is a horizontal rule.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
Below this line is another horizontal rule.
|
|
248
|
+
|
|
249
|
+
***
|
|
250
|
+
|
|
251
|
+
And here's one more made with asterisks.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Mathematical Expressions (if supported)
|
|
256
|
+
|
|
257
|
+
Inline math: $E = mc^2$
|
|
258
|
+
|
|
259
|
+
Block math:
|
|
260
|
+
$$
|
|
261
|
+
\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}
|
|
262
|
+
$$
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## HTML Elements (if raw HTML is enabled)
|
|
267
|
+
|
|
268
|
+
<div style="background-color: #f0f0f0; padding: 10px; border-radius: 5px;">
|
|
269
|
+
This is raw HTML content within markdown.
|
|
270
|
+
<br>
|
|
271
|
+
<strong>Bold text via HTML</strong>
|
|
272
|
+
<br>
|
|
273
|
+
<em>Italic text via HTML</em>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<details>
|
|
277
|
+
<summary>Click to expand</summary>
|
|
278
|
+
|
|
279
|
+
This content is hidden by default and can be toggled.
|
|
280
|
+
|
|
281
|
+
- Item 1
|
|
282
|
+
- Item 2
|
|
283
|
+
- Item 3
|
|
284
|
+
|
|
285
|
+
</details>
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Edge Cases
|
|
290
|
+
|
|
291
|
+
### Empty Code Block
|
|
292
|
+
\`\`\`
|
|
293
|
+
|
|
294
|
+
\`\`\`
|
|
295
|
+
|
|
296
|
+
### Code Block Without Language
|
|
297
|
+
\`\`\`
|
|
298
|
+
function noLanguageSpecified() {
|
|
299
|
+
return "This should still render";
|
|
300
|
+
}
|
|
301
|
+
\`\`\`
|
|
302
|
+
|
|
303
|
+
### Very Long Line
|
|
304
|
+
This is a very long line that should test how the renderer handles text wrapping and overflow in different containers and screen sizes to ensure proper responsive behavior.
|
|
305
|
+
|
|
306
|
+
### Unicode and Emojis
|
|
307
|
+
Unicode characters: café, naïve, résumé
|
|
308
|
+
Emojis: 🚀 ⚡ 💻 🎉 ✅ ❌ 🔥 💡
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Conclusion
|
|
313
|
+
|
|
314
|
+
This markdown document tests:
|
|
315
|
+
|
|
316
|
+
✅ **Headings** (H1-H6)
|
|
317
|
+
✅ **Text formatting** (bold, italic, strikethrough)
|
|
318
|
+
✅ **Links** (external, internal, email, auto-links)
|
|
319
|
+
✅ **Images** (valid and invalid URLs)
|
|
320
|
+
✅ **Lists** (ordered, unordered, nested, task lists)
|
|
321
|
+
✅ **Code blocks** (multiple languages, inline code)
|
|
322
|
+
✅ **Tables** (simple and complex)
|
|
323
|
+
✅ **Blockquotes**
|
|
324
|
+
✅ **Horizontal rules**
|
|
325
|
+
✅ **Special characters and escaping**
|
|
326
|
+
✅ **HTML elements** (if enabled)
|
|
327
|
+
✅ **Edge cases**
|
|
328
|
+
|
|
329
|
+
If all these elements render correctly, your markdown component is working properly! 🎉
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
*Last updated: ${new Date().toLocaleDateString()}*
|
|
334
|
+
`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './md-code-block'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './md-code-block'
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { memo, useState } from 'react'
|
|
2
|
+
import { Highlight, themes } from 'prism-react-renderer'
|
|
3
|
+
import type { PropsWithChildren } from 'react'
|
|
4
|
+
import { useTranslation } from 'react-i18next'
|
|
5
|
+
|
|
6
|
+
import { copyTextToClipboard, extractTextFromReactNodes } from '@/src/lib/utils'
|
|
7
|
+
|
|
8
|
+
export type MdCodeBlockProps = PropsWithChildren<{
|
|
9
|
+
inline?: boolean
|
|
10
|
+
className?: string
|
|
11
|
+
}>
|
|
12
|
+
|
|
13
|
+
const MdCodeBlock = memo(({ inline, className, children, ...props }: MdCodeBlockProps) => {
|
|
14
|
+
const { t } = useTranslation()
|
|
15
|
+
const [copied, setCopied] = useState(false)
|
|
16
|
+
const codeContent = extractTextFromReactNodes(children)
|
|
17
|
+
const match = /language-(\w+)/.exec(className || '')
|
|
18
|
+
const language = match ? match[1] : 'text'
|
|
19
|
+
|
|
20
|
+
const handleCopy = () => {
|
|
21
|
+
setCopied(true)
|
|
22
|
+
void copyTextToClipboard(codeContent)
|
|
23
|
+
.then(
|
|
24
|
+
(result) =>
|
|
25
|
+
new Promise((resolve, reject) => {
|
|
26
|
+
if (!result) return reject(new Error('copy failed'))
|
|
27
|
+
setTimeout(resolve, 2000)
|
|
28
|
+
})
|
|
29
|
+
)
|
|
30
|
+
.finally(() => setCopied(false))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (inline) {
|
|
34
|
+
return (
|
|
35
|
+
<code className='rounded bg-neutral-800 px-2 py-1 font-mono text-sm text-pink-600' {...props}>
|
|
36
|
+
{children}
|
|
37
|
+
</code>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className='group relative'>
|
|
43
|
+
<div className='flex items-center justify-between rounded-t-lg bg-neutral-800 px-4 py-2 text-sm text-neutral-300'>
|
|
44
|
+
<span className='font-mono text-xs uppercase tracking-wide'>{language}</span>
|
|
45
|
+
<button
|
|
46
|
+
onClick={handleCopy}
|
|
47
|
+
className='rounded bg-neutral-700 px-2 py-1 text-xs opacity-0 transition-opacity duration-200 hover:bg-neutral-600 hover:text-neutral-0 group-hover:opacity-100'>
|
|
48
|
+
{t(copied ? `general.buttons.copied` : 'general.buttons.copy')}
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<Highlight theme={themes.vsDark} code={codeContent} language={language}>
|
|
53
|
+
{({ className: highlightClassName, style, tokens, getLineProps, getTokenProps }) => (
|
|
54
|
+
<pre className={`${highlightClassName} overflow-x-auto rounded-b-lg p-4`} style={style}>
|
|
55
|
+
{tokens.map((line, i) => (
|
|
56
|
+
<div key={i} {...getLineProps({ line })}>
|
|
57
|
+
{line.map((token, key) => (
|
|
58
|
+
<span key={key} {...getTokenProps({ token })} />
|
|
59
|
+
))}
|
|
60
|
+
</div>
|
|
61
|
+
))}
|
|
62
|
+
</pre>
|
|
63
|
+
)}
|
|
64
|
+
</Highlight>
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
MdCodeBlock.displayName = 'MdCodeBlock'
|
|
70
|
+
|
|
71
|
+
export default MdCodeBlock
|