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.
Files changed (61) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/config/vitest/setupTests.ts +6 -1
  3. package/package.json +11 -2
  4. package/public/assets/images/default-image.png +0 -0
  5. package/src/@types/declarations.d.ts +12 -3
  6. package/src/config/dayjs/index.ts +2 -0
  7. package/src/config/dayjs/init.ts +28 -0
  8. package/src/config/dayjs/utils/format-fulldate.ts +7 -0
  9. package/src/config/dayjs/utils/format-time.ts +20 -0
  10. package/src/config/dayjs/utils/index.ts +2 -0
  11. package/src/config/styles/global.css +19 -1
  12. package/src/config/tanstack/query-provider.tsx +1 -1
  13. package/src/config/tests/handlers.ts +6 -0
  14. package/src/index.tsx +4 -0
  15. package/src/lib/components/index.ts +1 -0
  16. package/src/lib/components/markdownrenderer/__tests__/markdown.stub.ts +334 -0
  17. package/src/lib/components/markdownrenderer/components/index.ts +1 -0
  18. package/src/lib/components/markdownrenderer/components/md-code-block/index.ts +1 -0
  19. package/src/lib/components/markdownrenderer/components/md-code-block/md-code-block.tsx +71 -0
  20. package/src/lib/components/markdownrenderer/index.ts +2 -0
  21. package/src/lib/components/markdownrenderer/markdownrenderer.tsx +115 -0
  22. package/src/lib/utils/constants.ts +1 -1
  23. package/src/lib/utils/copy-text-to-clipboard.tsx +13 -0
  24. package/src/lib/utils/extract-text-from-react-nodes.ts +23 -0
  25. package/src/lib/utils/index.ts +3 -0
  26. package/src/lib/utils/urls.ts +20 -0
  27. package/src/modules/messages/__tests__/imessage-with-sender-data.builder.ts +113 -0
  28. package/src/modules/messages/__tests__/imessage-with-sender-data.mock.ts +15 -0
  29. package/src/modules/messages/components/index.ts +2 -0
  30. package/src/modules/messages/components/message-img/index.ts +1 -0
  31. package/src/modules/messages/components/message-img/message-img.tsx +47 -0
  32. package/src/modules/messages/components/message-item/index.ts +2 -0
  33. package/src/modules/messages/components/message-item/message-item.spec.tsx +26 -0
  34. package/src/modules/messages/components/message-item/message-item.tsx +15 -0
  35. package/src/modules/messages/components/messages-list/messages-list.tsx +46 -17
  36. package/src/modules/messages/hooks/index.ts +1 -0
  37. package/src/modules/messages/hooks/use-fetch-messages/index.ts +2 -0
  38. package/src/modules/messages/hooks/use-fetch-messages/use-fetch-messages.spec.tsx +46 -0
  39. package/src/modules/messages/hooks/use-fetch-messages/use-fetch-messages.tsx +103 -0
  40. package/src/modules/messages/service.ts +26 -3
  41. package/src/modules/messages/types.ts +33 -2
  42. package/src/modules/messages/utils/index.ts +1 -0
  43. package/src/modules/messages/utils/messages-parser/index.ts +1 -0
  44. package/src/modules/messages/utils/messages-parser/utils.ts +28 -0
  45. package/src/modules/profile/__tests__/profile-api-props.builder.ts +74 -0
  46. package/src/modules/profile/__tests__/profile-props.builder.ts +42 -0
  47. package/src/modules/profile/constants.ts +3 -0
  48. package/src/modules/profile/hooks/index.ts +1 -0
  49. package/src/modules/profile/hooks/use-get-profile/index.ts +1 -0
  50. package/src/modules/profile/hooks/use-get-profile/use-get-profile.spec.tsx +20 -0
  51. package/src/modules/profile/hooks/use-get-profile/use-get-profile.tsx +14 -0
  52. package/src/modules/profile/index.ts +4 -0
  53. package/src/modules/profile/service.tsx +19 -0
  54. package/src/modules/profile/types.ts +17 -0
  55. package/src/modules/widget/components/chat-page/chat-page.tsx +3 -3
  56. package/src/modules/widget/components/container/container.tsx +1 -1
  57. package/src/modules/widget/components/onboarding-page/onboarding-page.tsx +16 -14
  58. package/src/modules/widget/components/starter-page/starter-page.tsx +3 -3
  59. package/src/modules/widget/store/widget-settings.atom.ts +3 -1
  60. package/src/config/styles/shared-styles.module.css +0 -16
  61. 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.0",
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",
@@ -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,2 @@
1
+ export * from './init'
2
+ export * from './utils'
@@ -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,7 @@
1
+ import dayjs from 'dayjs'
2
+
3
+ export const formatFullDate = (timestamp?: dayjs.ConfigType) => {
4
+ const time = dayjs(timestamp)
5
+
6
+ return time.format('L')
7
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export * from './format-fulldate'
2
+ export * from './format-time'
@@ -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
- composes: scrollbar from './shared-styles.module.css';
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(
@@ -1,4 +1,5 @@
1
1
  export * from './button'
2
2
  export * from './errors'
3
3
  export * from './icons'
4
+ export * from './markdownrenderer'
4
5
  export * from './spinner'
@@ -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
+ ![React Logo](https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/512px-React-icon.svg.png)
38
+
39
+ ### Image with Alt Text
40
+ ![TypeScript Logo - A blue square with white TS text](https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Typescript_logo_2020.svg/512px-Typescript_logo_2020.svg.png)
41
+
42
+ ### Broken Image (for error handling)
43
+ ![This image doesn't exist](https://invalid-url.com/image.png)
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: &amp; &lt; &gt; &quot; &#39;
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
@@ -0,0 +1,2 @@
1
+ export * from './markdownrenderer'
2
+ export { default as MarkdownRenderer } from './markdownrenderer'