···
1
1
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
2
2
+
import { CollectionAccessType } from '@semble/types';
3
3
+
import type { Collection } from '@semble/types';
4
4
+
import ImageCard from './ImageCard';
5
5
+
import { AuthContext } from '@/hooks/useAuth';
6
6
+
import type { AuthContextType } from '@/hooks/useAuth';
7
7
+
8
8
+
const mockAuthUser = {
9
9
+
id: 'did:plc:mock123',
10
10
+
name: 'Test User',
11
11
+
handle: 'test.user',
12
12
+
avatarUrl: 'https://i.pravatar.cc/150?u=testuser',
13
13
+
};
14
14
+
15
15
+
const mockAuthValue: AuthContextType = {
16
16
+
user: mockAuthUser as any,
17
17
+
isAuthenticated: true,
18
18
+
isLoading: false,
19
19
+
refreshAuth: async () => {},
20
20
+
logout: async () => {},
21
21
+
};
22
22
+
23
23
+
const cardAuthor = {
24
24
+
id: 'did:plc:author456',
25
25
+
name: 'Jane Researcher',
26
26
+
handle: 'jane.researcher',
27
27
+
avatarUrl: 'https://i.pravatar.cc/150?u=jane',
28
28
+
description: 'Science writer and open-access advocate.',
29
29
+
followerCount: 214,
30
30
+
followingCount: 63,
31
31
+
};
32
32
+
33
33
+
const baseCollection: Collection = {
34
34
+
id: 'col-story-001',
35
35
+
uri: 'at://did:plc:colauthor789/app.semble.collection/3juzt2xkr5c2a',
36
36
+
name: 'Photography Inspiration',
37
37
+
author: {
38
38
+
id: 'did:plc:colauthor789',
39
39
+
name: 'Alex Curator',
40
40
+
handle: 'alex.curator',
41
41
+
avatarUrl: 'https://i.pravatar.cc/150?u=alex',
42
42
+
},
43
43
+
description: 'A curated collection of inspiring photography.',
44
44
+
accessType: CollectionAccessType.CLOSED,
45
45
+
cardCount: 18,
46
46
+
createdAt: '2024-09-15T12:00:00.000Z',
47
47
+
updatedAt: '2025-06-20T09:30:00.000Z',
48
48
+
isFollowing: false,
49
49
+
followerCount: 31,
50
50
+
};
51
51
+
52
52
+
const baseCardContent = {
53
53
+
url: 'https://example.com/photo-1',
54
54
+
title: 'Golden Hour Over the Mountains',
55
55
+
description: 'A breathtaking landscape captured during golden hour.',
56
56
+
imageUrl: 'https://picsum.photos/seed/imagecard1/800/600',
57
57
+
siteName: 'Unsplash',
58
58
+
type: 'article',
59
59
+
retrievedAt: '2025-06-01T10:00:00.000Z',
60
60
+
};
61
61
+
62
62
+
const meta: Meta<typeof ImageCard> = {
63
63
+
title: 'Features/Cards/ImageCard',
64
64
+
component: ImageCard,
65
65
+
decorators: [
66
66
+
(Story) => (
67
67
+
<AuthContext.Provider value={mockAuthValue}>
68
68
+
<div style={{ maxWidth: 360 }}>
69
69
+
<Story />
70
70
+
</div>
71
71
+
</AuthContext.Provider>
72
72
+
),
73
73
+
],
74
74
+
args: {
75
75
+
id: 'card-img-001',
76
76
+
cardContent: baseCardContent,
77
77
+
urlLibraryCount: 5,
78
78
+
urlIsInLibrary: false,
79
79
+
urlConnectionCount: 2,
80
80
+
urlIsConnected: false,
81
81
+
},
82
82
+
};
83
83
+
84
84
+
export default meta;
85
85
+
86
86
+
type Story = StoryObj<typeof ImageCard>;
87
87
+
88
88
+
export const Default: Story = {};
89
89
+
90
90
+
export const LongTitle: Story = {
91
91
+
args: {
92
92
+
id: 'card-img-longtitle',
93
93
+
cardContent: {
94
94
+
...baseCardContent,
95
95
+
url: 'https://example.com/photo-long-title',
96
96
+
title:
97
97
+
'An Incredibly Long Title That Should Be Truncated After Two Lines Because It Contains Way Too Much Text For a Card Header',
98
98
+
imageUrl: 'https://picsum.photos/seed/imagecard-long/800/600',
99
99
+
},
100
100
+
},
101
101
+
};
102
102
+
103
103
+
export const InLibrary: Story = {
104
104
+
args: {
105
105
+
id: 'card-img-library',
106
106
+
urlIsInLibrary: true,
107
107
+
urlLibraryCount: 42,
108
108
+
cardContent: {
109
109
+
...baseCardContent,
110
110
+
url: 'https://example.com/photo-library',
111
111
+
title: 'Misty Forest Path',
112
112
+
imageUrl: 'https://picsum.photos/seed/imagecard2/800/1000',
113
113
+
siteName: 'Pexels',
114
114
+
},
115
115
+
},
116
116
+
};
117
117
+
118
118
+
export const Connected: Story = {
119
119
+
args: {
120
120
+
id: 'card-img-connected',
121
121
+
urlIsConnected: true,
122
122
+
urlConnectionCount: 8,
123
123
+
cardContent: {
124
124
+
...baseCardContent,
125
125
+
url: 'https://example.com/photo-connected',
126
126
+
title: 'Urban Architecture at Night',
127
127
+
imageUrl: 'https://picsum.photos/seed/imagecard3/800/500',
128
128
+
siteName: 'Flickr',
129
129
+
},
130
130
+
},
131
131
+
};
132
132
+
133
133
+
export const WithNote: Story = {
134
134
+
args: {
135
135
+
id: 'card-img-note',
136
136
+
note: {
137
137
+
id: 'note-img-001',
138
138
+
text: 'Love the composition in this shot. The leading lines draw you right into the frame.',
139
139
+
},
140
140
+
cardContent: {
141
141
+
...baseCardContent,
142
142
+
url: 'https://example.com/photo-noted',
143
143
+
title: 'Desert Dunes at Sunset',
144
144
+
imageUrl: 'https://picsum.photos/seed/imagecard4/800/600',
145
145
+
},
146
146
+
},
147
147
+
};
148
148
+
149
149
+
export const WithCollection: Story = {
150
150
+
args: {
151
151
+
id: 'card-img-collection',
152
152
+
currentCollection: baseCollection,
153
153
+
cardAuthor: cardAuthor,
154
154
+
authorHandle: cardAuthor.handle,
155
155
+
cardContent: {
156
156
+
...baseCardContent,
157
157
+
url: 'https://example.com/photo-collection',
158
158
+
title: 'Coastal Cliffs of Ireland',
159
159
+
imageUrl: 'https://picsum.photos/seed/imagecard5/800/550',
160
160
+
siteName: 'National Geographic',
161
161
+
},
162
162
+
},
163
163
+
};
164
164
+
165
165
+
export const HighCounts: Story = {
166
166
+
args: {
167
167
+
id: 'card-img-highcounts',
168
168
+
urlLibraryCount: 1523,
169
169
+
urlIsInLibrary: true,
170
170
+
urlConnectionCount: 347,
171
171
+
urlIsConnected: true,
172
172
+
cardAuthor: cardAuthor,
173
173
+
authorHandle: cardAuthor.handle,
174
174
+
note: {
175
175
+
id: 'note-img-002',
176
176
+
text: 'One of the most shared images on the platform. The colors are unreal.',
177
177
+
},
178
178
+
cardContent: {
179
179
+
...baseCardContent,
180
180
+
url: 'https://example.com/photo-viral',
181
181
+
title: 'Aurora Borealis Over Iceland',
182
182
+
imageUrl: 'https://picsum.photos/seed/imagecard6/800/600',
183
183
+
siteName: 'Earth Porn',
184
184
+
},
185
185
+
},
186
186
+
};
187
187
+
188
188
+
export const WideImage: Story = {
189
189
+
args: {
190
190
+
id: 'card-img-wide',
191
191
+
cardContent: {
192
192
+
...baseCardContent,
193
193
+
url: 'https://example.com/photo-panoramic',
194
194
+
title: 'Panoramic View of the Grand Canyon',
195
195
+
imageUrl: 'https://picsum.photos/seed/imagecard7/1200/400',
196
196
+
siteName: 'Panoramics',
197
197
+
},
198
198
+
},
199
199
+
};
200
200
+
201
201
+
export const TallImage: Story = {
202
202
+
args: {
203
203
+
id: 'card-img-tall',
204
204
+
cardContent: {
205
205
+
...baseCardContent,
206
206
+
url: 'https://example.com/photo-portrait',
207
207
+
title: 'Narrow Alley in Venice',
208
208
+
imageUrl: 'https://picsum.photos/seed/imagecard8/400/900',
209
209
+
siteName: 'Travel Blog',
210
210
+
},
211
211
+
},
212
212
+
};
···
1
1
+
'use client';
2
2
+
3
3
+
import type { UrlCard, Collection, User } from '@/api-client';
4
4
+
import { Card, Image, Stack, Text } from '@mantine/core';
5
5
+
import UrlCardActions from '../urlCardActions/UrlCardActions';
6
6
+
import { CardSaveAnalyticsContext } from '@/features/analytics/types';
7
7
+
8
8
+
interface Props {
9
9
+
id: string;
10
10
+
cardContent: UrlCard['cardContent'];
11
11
+
note?: UrlCard['note'];
12
12
+
currentCollection?: Collection;
13
13
+
urlLibraryCount: number;
14
14
+
urlIsInLibrary?: boolean;
15
15
+
urlConnectionCount: number;
16
16
+
urlIsConnected?: boolean;
17
17
+
authorHandle?: string;
18
18
+
cardAuthor?: User;
19
19
+
viaCardId?: string;
20
20
+
analyticsContext?: CardSaveAnalyticsContext;
21
21
+
onClick?: (e: React.MouseEvent<HTMLElement>) => void;
22
22
+
}
23
23
+
24
24
+
export default function ImageCard(props: Props) {
25
25
+
return (
26
26
+
<Card
27
27
+
component="article"
28
28
+
radius="lg"
29
29
+
p="sm"
30
30
+
flex={1}
31
31
+
h="100%"
32
32
+
withBorder
33
33
+
style={{ cursor: 'pointer' }}
34
34
+
onClick={props.onClick}
35
35
+
>
36
36
+
<Stack gap="xs" justify="space-between" flex={1}>
37
37
+
<Stack gap="xs">
38
38
+
{props.cardContent.title && (
39
39
+
<Text fz="sm" c="gray" lineClamp={2}>
40
40
+
{props.cardContent.title}
41
41
+
</Text>
42
42
+
)}
43
43
+
44
44
+
{props.cardContent.imageUrl && (
45
45
+
<Image
46
46
+
src={props.cardContent.imageUrl}
47
47
+
alt={props.cardContent.title || 'Card image'}
48
48
+
w="100%"
49
49
+
mah={400}
50
50
+
radius="md"
51
51
+
fit="cover"
52
52
+
/>
53
53
+
)}
54
54
+
</Stack>
55
55
+
56
56
+
<Stack gap="xs" pt={0}>
57
57
+
<UrlCardActions
58
58
+
cardAuthor={props.cardAuthor}
59
59
+
cardContent={props.cardContent}
60
60
+
cardCount={props.urlLibraryCount}
61
61
+
id={props.id}
62
62
+
authorHandle={props.authorHandle}
63
63
+
note={props.note}
64
64
+
currentCollection={props.currentCollection}
65
65
+
urlLibraryCount={props.urlLibraryCount}
66
66
+
urlIsInLibrary={props.urlIsInLibrary ?? false}
67
67
+
urlConnectionCount={props.urlConnectionCount}
68
68
+
urlIsConnected={props.urlIsConnected}
69
69
+
viaCardId={props.viaCardId}
70
70
+
analyticsContext={props.analyticsContext}
71
71
+
/>
72
72
+
</Stack>
73
73
+
</Stack>
74
74
+
</Card>
75
75
+
);
76
76
+
}