···
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 UrlCard from './UrlCard';
5
5
+
import UrlCardSkeleton from './Skeleton.UrlCard';
6
6
+
import { AuthContext } from '@/hooks/useAuth';
7
7
+
import type { AuthContextType } from '@/hooks/useAuth';
8
8
+
9
9
+
const mockAuthUser = {
10
10
+
id: 'did:plc:mock123',
11
11
+
name: 'Test User',
12
12
+
handle: 'test.user',
13
13
+
avatarUrl: 'https://i.pravatar.cc/150?u=testuser',
14
14
+
};
15
15
+
16
16
+
const mockAuthValue: AuthContextType = {
17
17
+
user: mockAuthUser as any,
18
18
+
isAuthenticated: true,
19
19
+
isLoading: false,
20
20
+
refreshAuth: async () => {},
21
21
+
logout: async () => {},
22
22
+
};
23
23
+
24
24
+
const cardAuthor = {
25
25
+
id: 'did:plc:author456',
26
26
+
name: 'Jane Researcher',
27
27
+
handle: 'jane.researcher',
28
28
+
avatarUrl: 'https://i.pravatar.cc/150?u=jane',
29
29
+
description: 'Science writer and open-access advocate.',
30
30
+
followerCount: 214,
31
31
+
followingCount: 63,
32
32
+
};
33
33
+
34
34
+
const collectionAuthor = {
35
35
+
id: 'did:plc:colauthor789',
36
36
+
name: 'Alex Curator',
37
37
+
handle: 'alex.curator',
38
38
+
avatarUrl: 'https://i.pravatar.cc/150?u=alex',
39
39
+
};
40
40
+
41
41
+
const baseCollection: Collection = {
42
42
+
id: 'col-story-001',
43
43
+
uri: 'at://did:plc:colauthor789/app.semble.collection/3juzt2xkr5c2a',
44
44
+
name: 'Web Development Resources',
45
45
+
author: collectionAuthor,
46
46
+
description: 'A curated set of useful web development articles and tools.',
47
47
+
accessType: CollectionAccessType.CLOSED,
48
48
+
cardCount: 24,
49
49
+
createdAt: '2024-09-15T12:00:00.000Z',
50
50
+
updatedAt: '2025-06-20T09:30:00.000Z',
51
51
+
isFollowing: false,
52
52
+
followerCount: 42,
53
53
+
};
54
54
+
55
55
+
const baseCardContent = {
56
56
+
url: 'https://example.com/understanding-design-tokens',
57
57
+
title: 'Understanding Design Tokens: A Practical Guide',
58
58
+
description:
59
59
+
'Design tokens are the atoms of a design system — small, named entities that store visual-design decisions such as color, typography, and spacing.',
60
60
+
imageUrl: 'https://picsum.photos/seed/urlcard1/400/225',
61
61
+
siteName: 'Example Blog',
62
62
+
type: 'article',
63
63
+
retrievedAt: '2025-06-01T10:00:00.000Z',
64
64
+
};
65
65
+
66
66
+
const meta: Meta<typeof UrlCard> = {
67
67
+
title: 'Features/Cards/UrlCard',
68
68
+
component: UrlCard,
69
69
+
decorators: [
70
70
+
(Story) => (
71
71
+
<AuthContext.Provider value={mockAuthValue}>
72
72
+
<div style={{ maxWidth: 400 }}>
73
73
+
<Story />
74
74
+
</div>
75
75
+
</AuthContext.Provider>
76
76
+
),
77
77
+
],
78
78
+
args: {
79
79
+
id: 'card-default-001',
80
80
+
url: 'https://example.com/understanding-design-tokens',
81
81
+
uri: 'at://did:plc:author456/app.semble.card/3k1abc',
82
82
+
cardContent: baseCardContent,
83
83
+
urlLibraryCount: 5,
84
84
+
urlIsInLibrary: false,
85
85
+
urlConnectionCount: 2,
86
86
+
urlIsConnected: false,
87
87
+
showAuthor: false,
88
88
+
},
89
89
+
};
90
90
+
91
91
+
export default meta;
92
92
+
93
93
+
type Story = StoryObj<typeof UrlCard>;
94
94
+
95
95
+
export const Default: Story = {};
96
96
+
97
97
+
export const WithContributor: Story = {
98
98
+
args: {
99
99
+
id: 'card-author-002',
100
100
+
showAuthor: true,
101
101
+
cardAuthor: cardAuthor,
102
102
+
authorHandle: cardAuthor.handle,
103
103
+
},
104
104
+
};
105
105
+
106
106
+
export const WithNote: Story = {
107
107
+
args: {
108
108
+
id: 'card-note-003',
109
109
+
note: {
110
110
+
id: 'note-001',
111
111
+
text: 'This is an excellent primer on design tokens. Highly recommend reading the section on naming conventions — it changed how I structure my own token files.',
112
112
+
},
113
113
+
},
114
114
+
};
115
115
+
116
116
+
export const InLibrary: Story = {
117
117
+
args: {
118
118
+
id: 'card-library-004',
119
119
+
urlIsInLibrary: true,
120
120
+
urlLibraryCount: 37,
121
121
+
cardContent: {
122
122
+
...baseCardContent,
123
123
+
url: 'https://example.com/state-of-css-2025',
124
124
+
title: 'State of CSS 2025 Survey Results',
125
125
+
description:
126
126
+
'An overview of the most popular CSS features, frameworks, and tools according to thousands of developers worldwide.',
127
127
+
imageUrl: 'https://picsum.photos/seed/urlcard2/400/225',
128
128
+
siteName: 'State of CSS',
129
129
+
},
130
130
+
},
131
131
+
};
132
132
+
133
133
+
export const Connected: Story = {
134
134
+
args: {
135
135
+
id: 'card-connected-005',
136
136
+
urlIsConnected: true,
137
137
+
urlConnectionCount: 12,
138
138
+
cardContent: {
139
139
+
...baseCardContent,
140
140
+
url: 'https://example.com/react-server-components',
141
141
+
title: 'React Server Components Explained',
142
142
+
description:
143
143
+
'A deep dive into how React Server Components work under the hood, when to use them, and common pitfalls to avoid.',
144
144
+
imageUrl: 'https://picsum.photos/seed/urlcard3/400/225',
145
145
+
siteName: 'React Blog',
146
146
+
},
147
147
+
},
148
148
+
};
149
149
+
150
150
+
export const WithCollection: Story = {
151
151
+
args: {
152
152
+
id: 'card-collection-006',
153
153
+
currentCollection: baseCollection,
154
154
+
cardContent: {
155
155
+
...baseCardContent,
156
156
+
url: 'https://example.com/responsive-layout-patterns',
157
157
+
title: 'Modern Responsive Layout Patterns',
158
158
+
description:
159
159
+
'Move beyond media queries. Learn how to build truly fluid layouts using CSS Grid, Container Queries, and clamp().',
160
160
+
imageUrl: 'https://picsum.photos/seed/urlcard4/400/225',
161
161
+
siteName: 'CSS Tricks',
162
162
+
},
163
163
+
},
164
164
+
};
165
165
+
166
166
+
export const HighCounts: Story = {
167
167
+
args: {
168
168
+
id: 'card-highcounts-007',
169
169
+
urlLibraryCount: 2847,
170
170
+
urlIsInLibrary: true,
171
171
+
urlConnectionCount: 593,
172
172
+
urlIsConnected: true,
173
173
+
showAuthor: true,
174
174
+
cardAuthor: cardAuthor,
175
175
+
authorHandle: cardAuthor.handle,
176
176
+
cardContent: {
177
177
+
...baseCardContent,
178
178
+
url: 'https://example.com/attention-is-all-you-need',
179
179
+
title: 'Attention Is All You Need',
180
180
+
description:
181
181
+
'The dominant sequence transduction models are based on complex recurrent or convolutional neural networks that include an encoder and a decoder.',
182
182
+
imageUrl: 'https://picsum.photos/seed/urlcard5/400/225',
183
183
+
siteName: 'arXiv',
184
184
+
author: 'Vaswani et al.',
185
185
+
},
186
186
+
note: {
187
187
+
id: 'note-002',
188
188
+
text: 'The foundational transformer paper. Still worth re-reading every few months.',
189
189
+
},
190
190
+
},
191
191
+
};
192
192
+
193
193
+
export const Skeleton: Story = {
194
194
+
render: () => <UrlCardSkeleton />,
195
195
+
};
···
19
19
import { shouldCaptureAnalytics } from '@/features/analytics/utils';
20
20
import { ENABLE_AUTH_LOGGING } from '@/lib/auth/constants';
21
21
22
22
-
interface AuthContextType {
22
22
+
export interface AuthContextType {
23
23
user: GetProfileResponse | null;
24
24
isAuthenticated: boolean;
25
25
isLoading: boolean;
···
27
27
logout: () => Promise<void>;
28
28
}
29
29
30
30
-
const AuthContext = createContext<AuthContextType | undefined>(undefined);
30
30
+
export const AuthContext = createContext<AuthContextType | undefined>(
31
31
+
undefined,
32
32
+
);
31
33
32
34
export const AuthProvider = ({ children }: { children: ReactNode }) => {
33
35
const router = useRouter();