@linktr.ee/messaging-react 1.32.0 → 1.32.1

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 (30) hide show
  1. package/dist/Card-1CQEn-OT.js +171 -0
  2. package/dist/Card-1CQEn-OT.js.map +1 -0
  3. package/dist/Card-ClE_iExA.js +177 -0
  4. package/dist/Card-ClE_iExA.js.map +1 -0
  5. package/dist/{MediaPlayer-BCsdmsON.js → MediaPlayer-B9Ws2NeE.js} +115 -135
  6. package/dist/MediaPlayer-B9Ws2NeE.js.map +1 -0
  7. package/dist/index.d.ts +3 -2
  8. package/dist/index.js +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +136 -93
  12. package/src/components/LockedAttachment/components/Creator/Card.tsx +106 -106
  13. package/src/components/LockedAttachment/components/Creator/CardThumbnail.tsx +114 -0
  14. package/src/components/LockedAttachment/components/MediaPlayer.tsx +80 -66
  15. package/src/components/LockedAttachment/components/Visitor/Card.tsx +53 -78
  16. package/src/components/LockedAttachment/components/Visitor/CardActions.tsx +3 -3
  17. package/src/components/LockedAttachment/components/Visitor/CardThumbnail.tsx +81 -0
  18. package/src/components/LockedAttachment/types.ts +2 -0
  19. package/dist/Card-C5t3dZ5q.js +0 -350
  20. package/dist/Card-C5t3dZ5q.js.map +0 -1
  21. package/dist/Card-Cn2va-Qr.js +0 -205
  22. package/dist/Card-Cn2va-Qr.js.map +0 -1
  23. package/dist/MediaPlayer-BCsdmsON.js.map +0 -1
  24. package/src/components/LockedAttachment/components/Creator/CardAudioPreview.tsx +0 -161
  25. package/src/components/LockedAttachment/components/Creator/CardCollapsedThumbnail.tsx +0 -58
  26. package/src/components/LockedAttachment/components/Creator/CardImagePreview.tsx +0 -56
  27. package/src/components/LockedAttachment/components/Creator/CardVideoPreview.tsx +0 -91
  28. package/src/components/LockedAttachment/components/Visitor/CardImagePreview.tsx +0 -39
  29. package/src/components/LockedAttachment/components/Visitor/CardMediaPreview.tsx +0 -36
  30. package/src/components/LockedAttachment/components/Visitor/CardThumbnailPreview.tsx +0 -45
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "1.32.0",
3
+ "version": "1.32.1",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,17 +1,21 @@
1
1
  import type { Meta, StoryFn } from '@storybook/react'
2
- import React from 'react'
2
+ import React, { useState } from 'react'
3
3
 
4
4
  import LockedAttachment from '.'
5
5
 
6
6
  const VIDEO_THUMBNAIL_BLURRED = '/video-thumbnail-blurred.jpg'
7
+ const VIDEO_THUMBNAIL = '/video-thumbnail.jpg'
7
8
  const VIDEO_SOURCE = '/video-source.mp4'
8
9
 
9
10
  const IMAGE_THUMBNAIL_BLURRED = '/image-thumbnail-blurred.jpg'
11
+ const IMAGE_THUMBNAIL = '/image-thumbnail.jpg'
10
12
  const IMAGE_SOURCE = '/image-source.jpg'
11
13
 
14
+ const DOCUMENT_THUMBNAIL_BLURRED = '/document-thumbnail-blurred.jpg'
12
15
  const DOCUMENT_THUMBNAIL = '/document-thumbnail.jpg'
13
16
  const DOCUMENT_SOURCE = '/document-source.pdf'
14
17
 
18
+ const AUDIO_THUMBNAIL_BLURRED = '/audio-thumbnail-blurred.jpg'
15
19
  const AUDIO_THUMBNAIL = '/audio-thumbnail.jpg'
16
20
  const AUDIO_SOURCE = '/audio-source.mp3'
17
21
 
@@ -28,6 +32,7 @@ const VARIANTS = [
28
32
  mimeType: 'video/mp4',
29
33
  detail: '1:20',
30
34
  thumbnailUrl: VIDEO_THUMBNAIL_BLURRED,
35
+ thumbnailUnlockedUrl: VIDEO_THUMBNAIL,
31
36
  sourceUrl: VIDEO_SOURCE,
32
37
  },
33
38
  {
@@ -35,7 +40,8 @@ const VARIANTS = [
35
40
  title: 'Morning Meditation',
36
41
  mimeType: 'audio/mpeg',
37
42
  detail: '4:35',
38
- thumbnailUrl: AUDIO_THUMBNAIL,
43
+ thumbnailUrl: AUDIO_THUMBNAIL_BLURRED,
44
+ thumbnailUnlockedUrl: AUDIO_THUMBNAIL,
39
45
  sourceUrl: AUDIO_SOURCE,
40
46
  },
41
47
  {
@@ -44,6 +50,7 @@ const VARIANTS = [
44
50
  mimeType: 'image/jpeg',
45
51
  detail: '3.2 MB',
46
52
  thumbnailUrl: IMAGE_THUMBNAIL_BLURRED,
53
+ thumbnailUnlockedUrl: IMAGE_THUMBNAIL,
47
54
  sourceUrl: IMAGE_SOURCE,
48
55
  },
49
56
  {
@@ -51,15 +58,8 @@ const VARIANTS = [
51
58
  title: 'Strength Training Guide',
52
59
  mimeType: 'application/zip',
53
60
  detail: '14 files',
54
- thumbnailUrl: DOCUMENT_THUMBNAIL,
55
- sourceUrl: DOCUMENT_SOURCE,
56
- },
57
- {
58
- label: 'Unknown',
59
- title: 'Unknown Attachment',
60
- mimeType: 'application/octet-stream',
61
- detail: undefined,
62
- thumbnailUrl: undefined,
61
+ thumbnailUrl: DOCUMENT_THUMBNAIL_BLURRED,
62
+ thumbnailUnlockedUrl: DOCUMENT_THUMBNAIL,
63
63
  sourceUrl: DOCUMENT_SOURCE,
64
64
  },
65
65
  ]
@@ -70,7 +70,9 @@ const Table = ({ children }: { children: React.ReactNode }) => (
70
70
  </div>
71
71
  )
72
72
 
73
- const TableHead = ({ variants, }: {
73
+ const TableHead = ({
74
+ variants,
75
+ }: {
74
76
  variants: { label: string; mimeType: string }[]
75
77
  }) => (
76
78
  <thead>
@@ -88,69 +90,105 @@ const TableHead = ({ variants, }: {
88
90
  </thead>
89
91
  )
90
92
 
91
- export const Visitor: StoryFn = () => (
92
- <Table>
93
- <TableHead variants={VARIANTS} />
94
- <tbody>
95
- <tr>
96
- <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
97
- Locked
98
- </td>
99
- {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl }) => (
100
- <td key={mimeType} className="align-top">
101
- <LockedAttachment.Visitor
102
- title={title}
103
- amountText="AU$9.99"
104
- thumbnailUrl={thumbnailUrl}
105
- mimeType={mimeType}
106
- detail={detail}
107
- onUnlockClick={() => {}}
108
- onDownloadClick={() => {}}
109
- />
93
+ export const Visitor: StoryFn = () => {
94
+ const [isPaid, setPaid] = useState<string | undefined>()
95
+
96
+ return (
97
+ <Table>
98
+ <TableHead variants={VARIANTS} />
99
+ <tbody>
100
+ <tr>
101
+ <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
102
+ Locked
110
103
  </td>
111
- ))}
112
- </tr>
113
- <tr>
114
- <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
115
- Purchased
116
- </td>
117
- {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl }) => (
118
- <td key={mimeType} className="align-top">
119
- <LockedAttachment.Visitor
120
- title={title}
121
- amountText="AU$9.99"
122
- thumbnailUrl={thumbnailUrl}
123
- mimeType={mimeType}
124
- detail={detail}
125
- paymentStatus="paid"
126
- onUnlockClick={() => {}}
127
- onDownloadClick={() => {}}
128
- />
104
+ {VARIANTS.map(
105
+ ({
106
+ title,
107
+ mimeType,
108
+ detail,
109
+ thumbnailUrl,
110
+ thumbnailUnlockedUrl,
111
+ sourceUrl,
112
+ }) => (
113
+ <td key={mimeType} className="align-top">
114
+ <LockedAttachment.Visitor
115
+ title={title}
116
+ thumbnailUrl={thumbnailUrl}
117
+ mimeType={mimeType}
118
+ detail={detail}
119
+ paymentStatus={isPaid === mimeType ? 'paid' : undefined}
120
+ amountText="AU$9.99"
121
+ onUnlockClick={() => setPaid(mimeType)}
122
+ onDownloadClick={() => alert('Download clicked')}
123
+ onFetchSource={async () => {
124
+ return Promise.resolve({
125
+ sourceUrl: sourceUrl,
126
+ thumbnailUrl: thumbnailUnlockedUrl,
127
+ })
128
+ }}
129
+ />
130
+ </td>
131
+ )
132
+ )}
133
+ </tr>
134
+ <tr>
135
+ <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
136
+ Purchased
129
137
  </td>
130
- ))}
131
- </tr>
132
- <tr>
133
- <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
134
- Unlocked
135
- </td>
136
- {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl, sourceUrl }) => (
137
- <td key={mimeType} className="align-top">
138
- <LockedAttachment.Visitor
139
- title={title}
140
- thumbnailUrl={thumbnailUrl}
141
- mimeType={mimeType}
142
- detail={detail}
143
- amountText="AU$9.99"
144
- paymentStatus="paid"
145
- onUnlockClick={() => Promise.resolve({ sourceUrl })}
146
- onDownloadClick={() => {}}
147
- />
138
+ {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl }) => (
139
+ <td key={mimeType} className="align-top">
140
+ <LockedAttachment.Visitor
141
+ title={title}
142
+ thumbnailUrl={thumbnailUrl}
143
+ mimeType={mimeType}
144
+ detail={detail}
145
+ amountText="AU$9.99"
146
+ paymentStatus="paid"
147
+ onUnlockClick={() => alert('Unlock clicked')}
148
+ onDownloadClick={() => alert('Download clicked')}
149
+ />
150
+ </td>
151
+ ))}
152
+ </tr>
153
+ <tr>
154
+ <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
155
+ Unlocked
148
156
  </td>
149
- ))}
150
- </tr>
151
- </tbody>
152
- </Table>
153
- )
157
+ {VARIANTS.map(
158
+ ({
159
+ title,
160
+ mimeType,
161
+ detail,
162
+ thumbnailUrl,
163
+ thumbnailUnlockedUrl,
164
+ sourceUrl,
165
+ }) => (
166
+ <td key={mimeType} className="align-top">
167
+ <LockedAttachment.Visitor
168
+ title={title}
169
+ thumbnailUrl={thumbnailUrl}
170
+ mimeType={mimeType}
171
+ detail={detail}
172
+ amountText="AU$9.99"
173
+ paymentStatus="paid"
174
+ onUnlockClick={() => console.log('Unlock clicked')}
175
+ onDownloadClick={() => alert('Download clicked')}
176
+ onFetchSource={async () => {
177
+ await new Promise((resolve) => setTimeout(resolve, 500))
178
+ return Promise.resolve({
179
+ sourceUrl: sourceUrl,
180
+ thumbnailUrl: thumbnailUnlockedUrl,
181
+ })
182
+ }}
183
+ />
184
+ </td>
185
+ )
186
+ )}
187
+ </tr>
188
+ </tbody>
189
+ </Table>
190
+ )
191
+ }
154
192
 
155
193
  export const Creator: StoryFn = () => (
156
194
  <Table>
@@ -160,30 +198,39 @@ export const Creator: StoryFn = () => (
160
198
  <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
161
199
  Preview
162
200
  </td>
163
- {VARIANTS.map(({ mimeType, detail, thumbnailUrl, sourceUrl }) => (
164
- <td key={mimeType} className="align-top">
165
- <LockedAttachment.Creator
166
- isPreview={true}
167
- thumbnailUrl={thumbnailUrl}
168
- sourceUrl={sourceUrl}
169
- mimeType={mimeType}
170
- detail={detail}
171
- placeholderAmountText="A$0.00"
172
- onDismiss={undefined}
173
- />
174
- </td>
175
- ))}
201
+ {VARIANTS.map(
202
+ ({
203
+ mimeType,
204
+ detail,
205
+ thumbnailUrl,
206
+ thumbnailUnlockedUrl,
207
+ sourceUrl,
208
+ }) => (
209
+ <td key={mimeType} className="align-top">
210
+ <LockedAttachment.Creator
211
+ placeholderTitle="Attachment title"
212
+ placeholderAmountText="AU$0.00"
213
+ thumbnailUrl={thumbnailUrl}
214
+ mimeType={mimeType}
215
+ detail={detail}
216
+ onPreviewClick={() => ({
217
+ sourceUrl: sourceUrl,
218
+ thumbnailUrl: thumbnailUnlockedUrl,
219
+ })}
220
+ />
221
+ </td>
222
+ )
223
+ )}
176
224
  </tr>
177
225
  <tr>
178
226
  <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
179
227
  Pending
180
228
  </td>
181
- {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl, sourceUrl }) => (
229
+ {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl }) => (
182
230
  <td key={mimeType} className="align-top">
183
231
  <LockedAttachment.Creator
184
232
  title={title}
185
233
  thumbnailUrl={thumbnailUrl}
186
- sourceUrl={sourceUrl}
187
234
  mimeType={mimeType}
188
235
  detail={detail}
189
236
  amountText="AU$9.99"
@@ -196,16 +243,14 @@ export const Creator: StoryFn = () => (
196
243
  <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
197
244
  Sent
198
245
  </td>
199
- {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl, sourceUrl }) => (
246
+ {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl }) => (
200
247
  <td key={mimeType} className="align-top">
201
248
  <LockedAttachment.Creator
202
249
  title={title}
203
250
  thumbnailUrl={thumbnailUrl}
204
- sourceUrl={sourceUrl}
205
251
  mimeType={mimeType}
206
252
  detail={detail}
207
253
  amountText="AU$9.99"
208
- onDismiss={undefined}
209
254
  />
210
255
  </td>
211
256
  ))}
@@ -214,17 +259,15 @@ export const Creator: StoryFn = () => (
214
259
  <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
215
260
  Sold
216
261
  </td>
217
- {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl, sourceUrl }) => (
262
+ {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl }) => (
218
263
  <td key={mimeType} className="align-top">
219
264
  <LockedAttachment.Creator
220
265
  title={title}
221
266
  thumbnailUrl={thumbnailUrl}
222
- sourceUrl={sourceUrl}
223
267
  mimeType={mimeType}
224
268
  detail={detail}
225
269
  amountText="AU$9.99"
226
270
  paymentStatus="paid"
227
- onDismiss={undefined}
228
271
  />
229
272
  </td>
230
273
  ))}
@@ -1,105 +1,72 @@
1
1
  import {
2
2
  CheckCircleIcon,
3
+ EyeIcon,
4
+ EyeSlashIcon,
3
5
  LockIcon,
4
6
  LockOpenIcon,
5
7
  XIcon,
6
8
  } from '@phosphor-icons/react'
7
9
  import classNames from 'classnames'
8
- import React from 'react'
10
+ import React, { useCallback, useState } from 'react'
9
11
 
10
- import type { LockedAttachmentBaseProps } from '../../types'
12
+ import type {
13
+ LockedAttachmentBaseProps,
14
+ LockedAttachmentSource,
15
+ PaymentStatus,
16
+ } from '../../types'
11
17
  import { renderTypeIcon } from '../../utils/icons'
12
- import { getSourceType } from '../../utils/mimeType'
13
18
 
14
- import AudioPreview from './CardAudioPreview'
15
- import CollapsedThumbnail from './CardCollapsedThumbnail'
16
- import ImagePreview from './CardImagePreview'
17
- import VideoPreview from './CardVideoPreview'
19
+ import CardThumbnail from './CardThumbnail'
18
20
 
19
21
  export interface CreatorCardProps extends LockedAttachmentBaseProps {
20
- isPreview?: boolean
21
22
  placeholderTitle?: string
22
23
  placeholderAmountText?: string
23
- sourceUrl?: string
24
24
  onDismiss?: () => void
25
+ onPreviewClick?: () => LockedAttachmentSource
25
26
  }
26
27
 
27
- const CreatorCard: React.FC<CreatorCardProps> = (props) => {
28
- const {
29
- title,
30
- mimeType = 'application/octet-stream',
31
- thumbnailUrl,
32
- sourceUrl,
33
- detail,
34
- amountText,
35
- placeholderTitle = 'Attachment title',
36
- placeholderAmountText,
37
- paymentStatus,
38
- onDismiss,
39
- isPreview = false,
40
- } = props
41
- const sourceType = getSourceType(mimeType)
42
- const displayAmountText = amountText ?? placeholderAmountText
43
- const isPlaceholderAmount = !amountText && !!placeholderAmountText
44
-
45
- let mediaPreview: React.ReactNode
46
- if (isPreview && sourceType === 'audio') {
47
- mediaPreview = (
48
- <AudioPreview
49
- key={sourceUrl}
50
- sourceUrl={sourceUrl}
51
- thumbnailUrl={thumbnailUrl}
52
- mimeType={mimeType}
53
- />
54
- )
55
- } else if (isPreview && sourceType === 'video') {
56
- mediaPreview = (
57
- <VideoPreview
58
- key={sourceUrl}
59
- sourceUrl={sourceUrl}
60
- thumbnailUrl={thumbnailUrl}
61
- mimeType={mimeType}
28
+ const CreatorCard: React.FC<CreatorCardProps> = ({
29
+ title,
30
+ mimeType = 'application/octet-stream',
31
+ thumbnailUrl,
32
+ detail,
33
+ amountText,
34
+ placeholderTitle = 'Attachment title',
35
+ placeholderAmountText,
36
+ paymentStatus,
37
+ onDismiss,
38
+ onPreviewClick,
39
+ }) => {
40
+ const [source, setSource] = useState<LockedAttachmentSource | undefined>()
41
+
42
+ const effectiveSourceUrl = source?.sourceUrl
43
+ const effectiveThumbnailUrl = source?.thumbnailUrl ?? thumbnailUrl
44
+
45
+ const handleToggle = useCallback(() => {
46
+ if (source) {
47
+ setSource(undefined)
48
+ } else if (onPreviewClick) {
49
+ setSource(onPreviewClick())
50
+ }
51
+ }, [source, onPreviewClick])
52
+
53
+ return (
54
+ <div className="relative w-[280px] select-none overflow-hidden rounded-[24px] bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_4px_8px_rgba(0,0,0,0.06)]">
55
+ <CardHeader
56
+ onDismiss={onDismiss}
57
+ onPreviewClick={onPreviewClick}
58
+ sourceUrl={source?.sourceUrl}
59
+ paymentStatus={paymentStatus}
62
60
  />
63
- )
64
- } else if (isPreview && sourceType === 'image') {
65
- mediaPreview = (
66
- <ImagePreview
67
- key={sourceUrl}
68
- sourceUrl={sourceUrl}
69
- thumbnailUrl={thumbnailUrl}
70
- mimeType={mimeType}
61
+
62
+ <CardThumbnail
71
63
  title={title}
72
- />
73
- )
74
- } else {
75
- const lockedOverlayIcon = onDismiss
76
- ? undefined
77
- : paymentStatus === 'paid'
78
- ? LockOpenIcon
79
- : LockIcon
80
- mediaPreview = (
81
- <CollapsedThumbnail
82
- thumbnailUrl={thumbnailUrl}
64
+ sourceUrl={effectiveSourceUrl}
65
+ thumbnailUrl={effectiveThumbnailUrl}
83
66
  mimeType={mimeType}
84
- overlayIcon={lockedOverlayIcon}
85
- darkOverlay
67
+ onToggle={onPreviewClick ? handleToggle : undefined}
86
68
  />
87
- )
88
- }
89
69
 
90
- return (
91
- <div className="relative w-[280px] select-none overflow-hidden rounded-[24px] bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_4px_8px_rgba(0,0,0,0.06)]">
92
- {onDismiss && (
93
- <button
94
- type="button"
95
- onClick={onDismiss}
96
- className="absolute right-3 top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white"
97
- aria-label="Dismiss attachment"
98
- >
99
- <XIcon className="size-4" weight="bold" />
100
- </button>
101
- )}
102
- {mediaPreview}
103
70
  <div className="px-4 pb-3 pt-3">
104
71
  <p
105
72
  className={classNames('mb-1.5 truncate text-base font-medium', {
@@ -109,46 +76,38 @@ const CreatorCard: React.FC<CreatorCardProps> = (props) => {
109
76
  >
110
77
  {title || placeholderTitle}
111
78
  </p>
79
+
112
80
  <div className="flex items-center gap-1">
113
81
  {renderTypeIcon(mimeType, {
114
82
  className: 'size-5 shrink-0 text-black/55',
115
83
  weight: 'regular',
116
84
  })}
85
+
117
86
  {detail && (
118
87
  <span className="text-xs font-medium text-black/55">{detail}</span>
119
88
  )}
89
+
120
90
  {paymentStatus === 'paid' ? (
121
- <>
122
- <span className="text-xs font-medium text-black/55">•</span>
123
- <span className="text-xs font-medium text-[#008236]">
124
- Purchased
125
- </span>
91
+ <React.Fragment>
92
+ <span className="text-xs font-medium text-black/55">&bull;</span>
93
+ <span className="text-xs font-medium text-[#008236]">Sold</span>
126
94
  <CheckCircleIcon
127
95
  className="size-4 text-[#008236]"
128
96
  weight="bold"
129
97
  />
130
- </>
98
+ </React.Fragment>
131
99
  ) : (
132
- displayAmountText && (
133
- <>
134
- <span
135
- className={classNames('text-xs font-medium', {
136
- 'text-black/30': isPlaceholderAmount,
137
- 'text-black/55': !isPlaceholderAmount,
138
- })}
139
- >
140
-
141
- </span>
142
- <span
143
- className={classNames('text-xs font-medium', {
144
- 'text-black/30': isPlaceholderAmount,
145
- 'text-black/55': !isPlaceholderAmount,
146
- })}
147
- >
148
- {displayAmountText}
149
- </span>
150
- </>
151
- )
100
+ <React.Fragment>
101
+ <span className="text-xs font-medium text-black/55">&bull;</span>
102
+ <span
103
+ className={classNames('text-xs font-medium', {
104
+ 'text-black/30': !amountText,
105
+ 'text-black/55': !!amountText,
106
+ })}
107
+ >
108
+ {amountText || placeholderAmountText}
109
+ </span>
110
+ </React.Fragment>
152
111
  )}
153
112
  </div>
154
113
  </div>
@@ -156,4 +115,45 @@ const CreatorCard: React.FC<CreatorCardProps> = (props) => {
156
115
  )
157
116
  }
158
117
 
118
+ interface CardHeaderProps {
119
+ onDismiss?: () => void
120
+ onPreviewClick?: () => void
121
+ sourceUrl?: string
122
+ paymentStatus?: PaymentStatus
123
+ }
124
+
125
+ const CardHeader: React.FC<CardHeaderProps> = ({
126
+ onDismiss,
127
+ onPreviewClick,
128
+ sourceUrl,
129
+ paymentStatus,
130
+ }) => {
131
+ if (onDismiss) {
132
+ return (
133
+ <button
134
+ type="button"
135
+ onClick={onDismiss}
136
+ className="absolute top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white right-3"
137
+ aria-label="Dismiss attachment"
138
+ >
139
+ <XIcon className="size-4" weight="bold" />
140
+ </button>
141
+ )
142
+ }
143
+
144
+ const Icon = onPreviewClick
145
+ ? sourceUrl
146
+ ? EyeIcon
147
+ : EyeSlashIcon
148
+ : paymentStatus === 'paid'
149
+ ? LockOpenIcon
150
+ : LockIcon
151
+
152
+ return (
153
+ <div className="absolute top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white left-3">
154
+ <Icon className="size-4" weight="fill" />
155
+ </div>
156
+ )
157
+ }
158
+
159
159
  export default CreatorCard