rn-vs-lb 1.0.64 → 1.0.66

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 (113) hide show
  1. package/components/Button/Button.stories.tsx +1 -1
  2. package/components/Button/DeleteAccountButton.stories.tsx +13 -1
  3. package/components/Button/DeleteAccountButton.tsx +28 -8
  4. package/components/Button/PostButton.stories.tsx +1 -1
  5. package/components/Button/TelegramFeedbackLink.stories.tsx +7 -1
  6. package/components/Button/TelegramFeedbackLink.tsx +12 -4
  7. package/components/Cards/BusinessIdeaCard.stories.tsx +73 -0
  8. package/components/Cards/BusinessIdeaCard.tsx +251 -0
  9. package/components/Cards/EventCard.stories.tsx +4 -1
  10. package/components/Cards/EventCard.tsx +9 -2
  11. package/components/Cards/PlaceCard.stories.tsx +1 -1
  12. package/components/Cards/index.tsx +1 -0
  13. package/components/Chat/ChatItem.stories.tsx +1 -1
  14. package/components/Chat/ChatTab/ChatTabs.stories.tsx +1 -1
  15. package/components/Chat/ChatTab/SubTabButton.stories.tsx +1 -1
  16. package/components/Chat/InputMessage.stories.tsx +12 -1
  17. package/components/Chat/InputMessage.tsx +5 -3
  18. package/components/Chat/MessageItem/MessageItem.stories.tsx +1 -1
  19. package/components/Chat/PinnedMessagesBar/PinnedMessagesBar.stories.tsx +1 -1
  20. package/components/Gallery/AiAgentGallery.stories.tsx +1 -1
  21. package/components/Gallery/ProfileMediaGallery.stories.tsx +66 -0
  22. package/components/Gallery/ProfileMediaGallery.tsx +239 -0
  23. package/components/Gallery/ProfileMediaNavigationItem.tsx +73 -0
  24. package/components/Gallery/ProfileSelfiesGallery.pure.tsx +97 -0
  25. package/components/Gallery/ProfileSelfiesGallery.stories.tsx +86 -0
  26. package/components/Gallery/index.ts +4 -1
  27. package/components/Header/HeaderDefault.stories.tsx +1 -1
  28. package/components/Header/HeaderEdit.stories.tsx +1 -1
  29. package/components/Header/HeaderHome.stories.tsx +1 -1
  30. package/components/Header/HeaderSwitcher.stories.tsx +1 -1
  31. package/components/Header/HeaderWithImg.stories.tsx +10 -1
  32. package/components/Header/HeaderWithImg.tsx +27 -7
  33. package/components/Modals/GalleryModal.stories.tsx +1 -1
  34. package/components/Modals/GuestAiChatModal.stories.tsx +12 -4
  35. package/components/Modals/GuestAiChatModal.tsx +9 -3
  36. package/components/Modals/ReportModal.stories.tsx +35 -2
  37. package/components/Modals/ReportModal.tsx +23 -24
  38. package/components/Poll/CommentItem.stories.tsx +1 -1
  39. package/components/Poll/PollCardList.stories.tsx +5 -1
  40. package/components/Poll/PollCardList.tsx +4 -2
  41. package/components/Posts/EventCardList.stories.tsx +1 -1
  42. package/components/Posts/PlaceCardList.stories.tsx +1 -1
  43. package/components/Prank/HeroPrankCard.stories.tsx +47 -0
  44. package/components/Prank/HeroPrankCard.tsx +114 -0
  45. package/components/Prank/HomelessPrankPage.stories.tsx +58 -0
  46. package/components/Prank/UploadPromptCard.stories.tsx +55 -0
  47. package/components/Prank/UploadPromptCard.tsx +130 -0
  48. package/components/Prank/index.ts +5 -0
  49. package/components/Profile/ModalProfilePhoto.stories.tsx +1 -1
  50. package/components/Profile/ProfileCard/ProfileCard.stories.tsx +1 -1
  51. package/components/Profile/ProfilePhotoBanner.stories.tsx +11 -2
  52. package/components/Profile/ProfilePhotoBanner.tsx +5 -3
  53. package/components/Profile/ProfilePhotoUpload/ProfilePhotoUpload.stories.tsx +1 -1
  54. package/components/Profile/ProfileSummary/ProfileSummary.stories.tsx +52 -0
  55. package/components/Profile/ProfileSummary/ProfileSummary.tsx +188 -0
  56. package/components/Profile/ProfileTabs/UserProfileTabs.stories.tsx +1 -1
  57. package/components/Profile/index.ts +2 -0
  58. package/components/Screens/AppScreens.stories.tsx +572 -0
  59. package/components/Screens/HomeScreen.stories.tsx +159 -0
  60. package/components/Screens/LibraryScreen.stories.tsx +97 -0
  61. package/components/Settings/AppSettingsScreen.stories.tsx +134 -0
  62. package/components/Settings/AppSettingsScreen.tsx +215 -0
  63. package/components/Settings/SettingsHeader.tsx +37 -0
  64. package/components/Settings/SettingsListItem.tsx +142 -0
  65. package/components/Settings/SettingsManageAccountButton.tsx +58 -0
  66. package/components/Settings/SettingsOptionList.tsx +82 -0
  67. package/components/Settings/SettingsProfileCard.tsx +58 -0
  68. package/components/Settings/SettingsScreen.stories.tsx +66 -0
  69. package/components/Settings/SettingsScreen.tsx +129 -0
  70. package/components/Settings/SettingsSection.tsx +56 -0
  71. package/components/Settings/SettingsToggleItem.tsx +70 -0
  72. package/components/Settings/index.ts +23 -0
  73. package/components/Specialist/Hero.stories.tsx +2 -1
  74. package/components/Specialist/Hero.tsx +3 -1
  75. package/components/Specialist/PortfolioCarousel.stories.tsx +1 -1
  76. package/components/Specialist/ServicesList.stories.tsx +2 -1
  77. package/components/Specialist/ServicesList.tsx +4 -3
  78. package/components/Tooltip/DangerTooltip.stories.tsx +1 -1
  79. package/components/Tooltip/InfoTooltip.stories.tsx +1 -1
  80. package/components/Tooltip/InfoTooltipBase.stories.tsx +1 -1
  81. package/components/Tooltip/SucceedTooltip.stories.tsx +1 -1
  82. package/components/Tooltip/WarningTooltip.stories.tsx +1 -1
  83. package/components/UI/DeletedState.stories.tsx +6 -1
  84. package/components/UI/DeletedState.tsx +12 -3
  85. package/components/UI/DescriptionMore.stories.tsx +2 -0
  86. package/components/UI/DescriptionMore.tsx +13 -3
  87. package/components/UI/EmptyState.stories.tsx +4 -1
  88. package/components/UI/EmptyState.tsx +3 -2
  89. package/components/UI/HorizontalCardSection.stories.tsx +85 -0
  90. package/components/UI/HorizontalCardSection.tsx +199 -0
  91. package/components/UI/NoAuth.stories.tsx +3 -0
  92. package/components/UI/NoAuth.tsx +7 -6
  93. package/components/UI/ParticipantItem.stories.tsx +21 -0
  94. package/components/UI/ParticipantItem.tsx +30 -6
  95. package/components/UI/TabBar/BottomTabBar.stories.tsx +87 -0
  96. package/components/UI/TabBar/BottomTabBar.tsx +128 -0
  97. package/components/UI/ThemeSwitcher.stories.tsx +5 -1
  98. package/components/UI/ThemeSwitcher.tsx +7 -2
  99. package/components/UI/UpdateRequiredView.stories.tsx +2 -0
  100. package/components/UI/UpdateRequiredView.tsx +3 -2
  101. package/components/UI/index.ts +4 -0
  102. package/components/UserCards/AiAgentHeroCard.stories.tsx +66 -0
  103. package/components/UserCards/AiAgentHeroCard.tsx +242 -0
  104. package/components/UserCards/Organazer.tsx +3 -2
  105. package/components/UserCards/Organizer.stories.tsx +5 -1
  106. package/components/UserCards/SpecialistCard.stories.tsx +3 -1
  107. package/components/UserCards/SpecialistCard.tsx +3 -2
  108. package/components/UserCards/StoryCard.stories.tsx +1 -1
  109. package/components/UserCards/UserProfileCard.stories.tsx +1 -1
  110. package/components/UserCards/UserRow.stories.tsx +1 -1
  111. package/components/UserCards/index.ts +2 -0
  112. package/components/index.ts +2 -0
  113. package/package.json +2 -1
@@ -8,7 +8,7 @@ import InputMessage, { ImageAsset } from './InputMessage';
8
8
  type Props = React.ComponentProps<typeof InputMessage>;
9
9
 
10
10
  const meta: Meta<Props> = {
11
- title: 'Chat/InputMessage',
11
+ title: 'Features/Chat/InputMessage',
12
12
  component: InputMessage,
13
13
  decorators: [
14
14
  (Story) => (
@@ -87,18 +87,21 @@ const Template: StoryFn<Props> = (args) => {
87
87
  export const Default = Template.bind({});
88
88
  Default.args = {
89
89
  placeholder: 'Message',
90
+ editingLabel: 'Editing message',
90
91
  maxImages: 1,
91
92
  };
92
93
 
93
94
  export const AttachmentsDisabled = Template.bind({});
94
95
  AttachmentsDisabled.args = {
95
96
  placeholder: 'No attachments allowed',
97
+ editingLabel: 'Editing message',
96
98
  enableImageAttachment: false,
97
99
  };
98
100
 
99
101
  export const WithAttachments = Template.bind({});
100
102
  WithAttachments.args = {
101
103
  placeholder: 'Attach up to 2 images',
104
+ editingLabel: 'Editing message',
102
105
  maxImages: 2,
103
106
  };
104
107
  WithAttachments.render = (args) => {
@@ -118,6 +121,7 @@ WithAttachments.render = (args) => {
118
121
  return imgs;
119
122
  }}
120
123
  onMaxImagesExceeded={(max) => Alert.alert('Max images exceeded', `Allowed: ${max}`)}
124
+ editingLabel="Editing message"
121
125
  />
122
126
  );
123
127
  };
@@ -137,6 +141,8 @@ export const ReplyMode: StoryFn = () => {
137
141
  onCancelReply={replyCancelHandler}
138
142
  onTyping={typingHandler}
139
143
  onStopTyping={stopTypingHandler}
144
+ placeholder="Message"
145
+ editingLabel="Editing message"
140
146
  />
141
147
  );
142
148
  };
@@ -152,6 +158,8 @@ export const EditMode: StoryFn = () => {
152
158
  onCancelEdit={editCancelHandler}
153
159
  onTyping={typingHandler}
154
160
  onStopTyping={stopTypingHandler}
161
+ placeholder="Message"
162
+ editingLabel="Editing message"
155
163
  />
156
164
  );
157
165
  };
@@ -176,6 +184,8 @@ export const SendingControlled: StoryFn = () => {
176
184
  maxImages={1}
177
185
  onTyping={typingHandler}
178
186
  onStopTyping={stopTypingHandler}
187
+ placeholder="Message"
188
+ editingLabel="Editing message"
179
189
  />
180
190
  );
181
191
  };
@@ -200,6 +210,7 @@ export const ManyInputsDemo: StoryFn = () => {
200
210
  placeholder={`Message #${idx + 1}`}
201
211
  onTyping={() => logEvent(`typing-${idx}`)}
202
212
  onStopTyping={() => logEvent(`stop-typing-${idx}`)}
213
+ editingLabel="Editing message"
203
214
  />
204
215
  ))}
205
216
  </ScrollView>
@@ -33,7 +33,8 @@ interface InputMessageProps {
33
33
  onStopTyping?: () => void; // будет вызвано через ~2s тишины
34
34
 
35
35
  // Placeholder
36
- placeholder?: string;
36
+ placeholder: string;
37
+ editingLabel: string;
37
38
  }
38
39
 
39
40
  export const InputMessage: FC<InputMessageProps> = ({
@@ -56,7 +57,8 @@ export const InputMessage: FC<InputMessageProps> = ({
56
57
  onTyping,
57
58
  onStopTyping,
58
59
 
59
- placeholder = 'Message',
60
+ placeholder,
61
+ editingLabel,
60
62
  }) => {
61
63
  const { theme, typography } = useTheme();
62
64
  const styles = getStyles(theme);
@@ -119,7 +121,7 @@ export const InputMessage: FC<InputMessageProps> = ({
119
121
  {/* Edit */}
120
122
  {editMessage && (
121
123
  <View style={[styles.replyContainer, { borderLeftColor: theme.primaryLight }]}>
122
- <Text style={styles.replyLabel}>Editing message</Text>
124
+ <Text style={styles.replyLabel}>{editingLabel}</Text>
123
125
  <View style={styles.replyContent}>
124
126
  <Text numberOfLines={1} style={styles.replyText}>
125
127
  {editMessage.content ?? ''}
@@ -7,7 +7,7 @@ import MessageItem from './MessageItem';
7
7
  import { LinkPreviewData, MessageDTO, MessageItemProps } from './types';
8
8
 
9
9
  const meta: Meta<MessageItemProps> = {
10
- title: 'Chat/MessageItem',
10
+ title: 'Features/Chat/MessageItem',
11
11
  component: MessageItem,
12
12
  decorators: [
13
13
  (Story) => (
@@ -8,7 +8,7 @@ import { MessageDTO } from '../../../types/message';
8
8
  type Props = React.ComponentProps<typeof PinnedMessagesBar>;
9
9
 
10
10
  const meta: Meta<Props> = {
11
- title: 'Chat/PinnedMessagesBar',
11
+ title: 'Features/Chat/PinnedMessagesBar',
12
12
  component: PinnedMessagesBar,
13
13
  decorators: [
14
14
  (Story) => (
@@ -7,7 +7,7 @@ import { AiAgentGalleryView } from './AiAgentGallery.pure';
7
7
  type Props = React.ComponentProps<typeof AiAgentGalleryView>;
8
8
 
9
9
  const meta: Meta<Props> = {
10
- title: 'Gallery/AiAgentGallery',
10
+ title: 'Features/Gallery/AiAgentGallery',
11
11
  component: AiAgentGalleryView,
12
12
  decorators: [
13
13
  (Story) => (
@@ -0,0 +1,66 @@
1
+ import React, { useState } from "react";
2
+ import { Meta, StoryFn } from "@storybook/react";
3
+ import { View } from "react-native";
4
+ import { ThemeProvider } from "../../theme";
5
+ import { ProfileMediaGallery, ProfileMediaGalleryProps } from "./ProfileMediaGallery";
6
+
7
+ const meta: Meta<typeof ProfileMediaGallery> = {
8
+ title: 'Features/Gallery/ProfileMediaGallery',
9
+ component: ProfileMediaGallery,
10
+ decorators: [
11
+ (Story) => (
12
+ <ThemeProvider>
13
+ <View style={{ padding: 24 }}>
14
+ <Story />
15
+ </View>
16
+ </ThemeProvider>
17
+ ),
18
+ ],
19
+ };
20
+
21
+ export default meta;
22
+
23
+ const TABS: ProfileMediaGalleryProps["tabs"] = [
24
+ { key: "photos", label: "Photos", iconName: "collections" },
25
+ { key: "videos", label: "Videos", iconName: "videocam" },
26
+ { key: "music", label: "Music", iconName: "music-note" },
27
+ { key: "albums", label: "Albums", iconName: "photo-album" },
28
+ ];
29
+
30
+ const Template: StoryFn<ProfileMediaGalleryProps> = (args) => (
31
+ <ProfileMediaGallery {...args} />
32
+ );
33
+
34
+ export const Default = Template.bind({});
35
+ Default.args = {
36
+ tabs: TABS,
37
+ images: [
38
+ "https://images.unsplash.com/photo-1503023345310-bd7c1de61c7d?auto=format&fit=crop&w=600&q=60",
39
+ "https://images.unsplash.com/photo-1520813792240-56fc4a3765a7?auto=format&fit=crop&w=600&q=60",
40
+ "https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=600&q=60",
41
+ "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=600&q=60",
42
+ "https://images.unsplash.com/photo-1492562080023-ab3db95bfbce?auto=format&fit=crop&w=600&q=60",
43
+ "https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?auto=format&fit=crop&w=600&q=60",
44
+ ],
45
+ };
46
+
47
+ export const ControlledTabs: StoryFn<ProfileMediaGalleryProps> = ({ ...rest }) => {
48
+ const [activeTabKey, setActiveTabKey] = useState(rest.activeTabKey ?? TABS[0].key);
49
+
50
+ return (
51
+ <ProfileMediaGallery
52
+ {...rest}
53
+ tabs={rest.tabs ?? TABS}
54
+ activeTabKey={activeTabKey}
55
+ onTabPress={(tab, index) => {
56
+ setActiveTabKey(tab.key);
57
+ rest.onTabPress?.(tab, index);
58
+ }}
59
+ />
60
+ );
61
+ };
62
+
63
+ ControlledTabs.args = {
64
+ tabs: TABS,
65
+ images: Default.args?.images ?? [],
66
+ };
@@ -0,0 +1,239 @@
1
+ import React, { useMemo, useState, useEffect } from "react";
2
+ import {
3
+ Image,
4
+ Pressable,
5
+ ScrollView,
6
+ StyleSheet,
7
+ Text,
8
+ View,
9
+ useWindowDimensions,
10
+ } from "react-native";
11
+ import { ThemeType, SizesType, TypographytType, useTheme } from "../../theme";
12
+ import { GalleryModal } from "../Modals";
13
+ import { ProfileMediaNavigationItem, ProfileMediaNavigationItemProps } from "./ProfileMediaNavigationItem";
14
+
15
+ type TabItem = Omit<ProfileMediaNavigationItemProps, "isActive" | "onPress"> & {
16
+ key: string;
17
+ };
18
+
19
+ export type ProfileMediaGalleryProps = {
20
+ tabs: TabItem[];
21
+ activeTabKey?: string;
22
+ onTabPress?: (tab: TabItem, index: number) => void;
23
+ images: string[];
24
+ extraInfoLabel?: string;
25
+ extraInfoActionLabel?: string;
26
+ onExtraInfoPress?: () => void;
27
+ uploadButtonLabel?: string;
28
+ showAllButtonLabel?: string;
29
+ onUploadPress?: () => void;
30
+ onShowAllPress?: () => void;
31
+ };
32
+
33
+ const FALLBACK_IMAGES = [
34
+ "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&w=600&q=60",
35
+ "https://images.unsplash.com/photo-1503023345310-bd7c1de61c7d?auto=format&fit=crop&w=600&q=60",
36
+ "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=600&q=60",
37
+ "https://images.unsplash.com/photo-1557862921-37829c790f19?auto=format&fit=crop&w=600&q=60",
38
+ "https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=600&q=60",
39
+ "https://images.unsplash.com/photo-1520813792240-56fc4a3765a7?auto=format&fit=crop&w=600&q=60",
40
+ ];
41
+
42
+ export const ProfileMediaGallery: React.FC<ProfileMediaGalleryProps> = ({
43
+ tabs = [],
44
+ activeTabKey,
45
+ onTabPress,
46
+ images = [],
47
+ extraInfoLabel = "+14 photo tags",
48
+ extraInfoActionLabel = "View",
49
+ onExtraInfoPress,
50
+ uploadButtonLabel = "Upload photo",
51
+ showAllButtonLabel = "Show all",
52
+ onUploadPress,
53
+ onShowAllPress,
54
+ }) => {
55
+ const { theme, sizes, typography } = useTheme();
56
+ const { width } = useWindowDimensions();
57
+ const styles = useMemo(() => getStyles(theme, sizes, typography), [theme, sizes, typography]);
58
+ const [internalActiveKey, setInternalActiveKey] = useState<string>(
59
+ activeTabKey ?? tabs[0]?.key ?? ""
60
+ );
61
+
62
+ useEffect(() => {
63
+ if (activeTabKey) {
64
+ setInternalActiveKey(activeTabKey);
65
+ }
66
+ }, [activeTabKey]);
67
+
68
+ const [modalVisible, setModalVisible] = useState(false);
69
+ const [modalInitialIndex, setModalInitialIndex] = useState(0);
70
+
71
+ const displayImages = images.length ? images.slice(0, 6) : FALLBACK_IMAGES;
72
+
73
+ const contentHorizontalPadding = (sizes.lg as number) * 2;
74
+ const gutter = (sizes.xs as number) * 2;
75
+ const imageSize = Math.max(
76
+ 72,
77
+ Math.floor((width - contentHorizontalPadding - gutter * 2) / 3)
78
+ );
79
+
80
+ const handleTabPress = (tab: TabItem, index: number) => {
81
+ if (!activeTabKey) {
82
+ setInternalActiveKey(tab.key);
83
+ }
84
+ onTabPress?.(tab, index);
85
+ };
86
+
87
+ const handleOpenImage = (index: number) => {
88
+ setModalInitialIndex(index);
89
+ setModalVisible(true);
90
+ };
91
+
92
+ return (
93
+ <View style={styles.wrapper}>
94
+ <ScrollView
95
+ horizontal
96
+ showsHorizontalScrollIndicator={false}
97
+ contentContainerStyle={styles.navigationContainer}
98
+ >
99
+ {tabs.map((tab, index) => (
100
+ <ProfileMediaNavigationItem
101
+ key={tab.key}
102
+ label={tab.label}
103
+ iconName={tab.iconName}
104
+ isActive={internalActiveKey === tab.key}
105
+ onPress={() => handleTabPress(tab, index)}
106
+ />
107
+ ))}
108
+ </ScrollView>
109
+
110
+ <View style={styles.galleryContainer}>
111
+ {displayImages.map((uri, index) => (
112
+ <Pressable
113
+ key={`${uri}-${index}`}
114
+ onPress={() => handleOpenImage(index)}
115
+ style={[styles.imageWrapper, { width: imageSize, height: imageSize }]}
116
+ android_ripple={{ color: "#00000022" }}
117
+ >
118
+ <Image
119
+ source={{ uri }}
120
+ style={styles.image}
121
+ resizeMode="cover"
122
+ />
123
+ </Pressable>
124
+ ))}
125
+ </View>
126
+
127
+ <View style={styles.infoRow}>
128
+ <Text style={styles.infoLabel}>{extraInfoLabel}</Text>
129
+ <Pressable onPress={onExtraInfoPress}>
130
+ <Text style={styles.infoLink}>{extraInfoActionLabel}</Text>
131
+ </Pressable>
132
+ </View>
133
+
134
+ <View style={styles.actionsRow}>
135
+ <Pressable
136
+ onPress={onUploadPress}
137
+ style={[styles.actionButton, styles.primaryActionButton]}
138
+ >
139
+ <Text style={[styles.actionButtonText, styles.primaryActionButtonText]}>
140
+ {uploadButtonLabel}
141
+ </Text>
142
+ </Pressable>
143
+ <Pressable
144
+ onPress={onShowAllPress}
145
+ style={[styles.actionButton, styles.secondaryActionButton]}
146
+ >
147
+ <Text style={[styles.actionButtonText, styles.secondaryActionButtonText]}>
148
+ {showAllButtonLabel}
149
+ </Text>
150
+ </Pressable>
151
+ </View>
152
+
153
+ <GalleryModal
154
+ visible={modalVisible}
155
+ images={displayImages}
156
+ initialIndex={modalInitialIndex}
157
+ onRequestClose={() => setModalVisible(false)}
158
+ />
159
+ </View>
160
+ );
161
+ };
162
+
163
+ const getStyles = (theme: ThemeType, sizes: SizesType, typography: TypographytType) =>
164
+ StyleSheet.create({
165
+ wrapper: {
166
+ backgroundColor: theme.card,
167
+ borderRadius: sizes.radius_lg as number,
168
+ paddingVertical: sizes.lg as number,
169
+ paddingHorizontal: sizes.lg as number,
170
+ },
171
+ navigationContainer: {
172
+ paddingBottom: sizes.sm as number,
173
+ },
174
+ galleryContainer: {
175
+ flexDirection: "row",
176
+ flexWrap: "wrap",
177
+ justifyContent: "space-between",
178
+ },
179
+ imageWrapper: {
180
+ borderRadius: sizes.radius_sm as number,
181
+ overflow: "hidden",
182
+ marginBottom: sizes.sm as number,
183
+ backgroundColor: theme.backgroundSecond,
184
+ },
185
+ image: {
186
+ width: "100%",
187
+ height: "100%",
188
+ },
189
+ infoRow: {
190
+ flexDirection: "row",
191
+ alignItems: "center",
192
+ justifyContent: "space-between",
193
+ marginTop: sizes.xs as number,
194
+ marginBottom: sizes.md as number,
195
+ },
196
+ infoLabel: {
197
+ ...(typography.bodySm as object),
198
+ color: theme.text,
199
+ fontWeight: "500",
200
+ },
201
+ infoLink: {
202
+ ...(typography.textLink as object),
203
+ fontWeight: "600",
204
+ },
205
+ actionsRow: {
206
+ flexDirection: "row",
207
+ justifyContent: "space-between",
208
+ alignItems: "center",
209
+ marginTop: sizes.xs as number,
210
+ },
211
+ actionButton: {
212
+ flex: 1,
213
+ borderRadius: sizes.radius_sm as number,
214
+ paddingVertical: sizes.sm as number,
215
+ alignItems: "center",
216
+ justifyContent: "center",
217
+ },
218
+ primaryActionButton: {
219
+ backgroundColor: theme.primary,
220
+ marginRight: sizes.sm as number,
221
+ },
222
+ secondaryActionButton: {
223
+ backgroundColor: theme.backgroundSecond,
224
+ borderWidth: 1,
225
+ borderColor: theme.border,
226
+ },
227
+ actionButtonText: {
228
+ ...(typography.bodySm as object),
229
+ fontWeight: "600",
230
+ },
231
+ primaryActionButtonText: {
232
+ color: theme.white,
233
+ },
234
+ secondaryActionButtonText: {
235
+ color: theme.text,
236
+ },
237
+ });
238
+
239
+ export default ProfileMediaGallery;
@@ -0,0 +1,73 @@
1
+ import React, { memo, useMemo } from "react";
2
+ import { Pressable, StyleSheet, Text, View } from "react-native";
3
+ import { MaterialIcons } from "@expo/vector-icons";
4
+ import { ThemeType, SizesType, TypographytType, useTheme } from "../../theme";
5
+
6
+ type IconName = React.ComponentProps<typeof MaterialIcons>["name"];
7
+
8
+ export type ProfileMediaNavigationItemProps = {
9
+ label: string;
10
+ iconName?: IconName;
11
+ isActive?: boolean;
12
+ onPress?: () => void;
13
+ };
14
+
15
+ export const ProfileMediaNavigationItem = memo(
16
+ ({ label, iconName = "photo", isActive = false, onPress }: ProfileMediaNavigationItemProps) => {
17
+ const { theme, sizes, typography } = useTheme();
18
+
19
+ const styles = useMemo(() => getStyles(theme, sizes, typography), [theme, sizes, typography]);
20
+
21
+ return (
22
+ <Pressable
23
+ onPress={onPress}
24
+ style={[styles.container, isActive ? styles.containerActive : styles.containerInactive]}
25
+ accessibilityRole="button"
26
+ accessibilityState={{ selected: isActive }}
27
+ >
28
+ {iconName ? (
29
+ <View style={styles.iconWrapper}>
30
+ <MaterialIcons
31
+ name={iconName}
32
+ size={20}
33
+ color={isActive ? theme.primary : theme.greyText}
34
+ />
35
+ </View>
36
+ ) : null}
37
+ <Text style={[styles.label, { color: isActive ? theme.primary : theme.greyText }]}>{label}</Text>
38
+ </Pressable>
39
+ );
40
+ }
41
+ );
42
+
43
+ ProfileMediaNavigationItem.displayName = "ProfileMediaNavigationItem";
44
+
45
+ const getStyles = (theme: ThemeType, sizes: SizesType, typography: TypographytType) =>
46
+ StyleSheet.create({
47
+ container: {
48
+ flexDirection: "row",
49
+ alignItems: "center",
50
+ borderRadius: sizes.radius_sm as number,
51
+ paddingHorizontal: sizes.md as number,
52
+ paddingVertical: sizes.xs as number,
53
+ marginRight: sizes.sm as number,
54
+ borderWidth: 1,
55
+ },
56
+ containerActive: {
57
+ backgroundColor: theme.white,
58
+ borderColor: theme.primary,
59
+ },
60
+ containerInactive: {
61
+ backgroundColor: theme.backgroundSecond,
62
+ borderColor: theme.border,
63
+ },
64
+ iconWrapper: {
65
+ marginRight: sizes.xs as number,
66
+ },
67
+ label: {
68
+ ...(typography.bodySm as object),
69
+ fontWeight: "500",
70
+ },
71
+ });
72
+
73
+ export default ProfileMediaNavigationItem;
@@ -0,0 +1,97 @@
1
+ import React, { memo, useMemo } from "react";
2
+ import { Image, Pressable, StyleSheet, View, StyleProp, ViewStyle } from "react-native";
3
+ import { GalleryModal } from "../Modals";
4
+ import { ThemeType, SizesType, useTheme } from "../../theme";
5
+
6
+ type BaseProps = {
7
+ photos: string[];
8
+ columns?: number;
9
+ itemSize: number;
10
+ gap?: number;
11
+ visible: boolean;
12
+ initialIndex: number;
13
+ onOpenAt: (index: number) => void;
14
+ onClose: () => void;
15
+ };
16
+
17
+ export type ProfileSelfiesGalleryViewProps = BaseProps & {
18
+ style?: StyleProp<ViewStyle>;
19
+ };
20
+
21
+ export const ProfileSelfiesGalleryView = memo(
22
+ ({
23
+ photos,
24
+ columns = 2,
25
+ itemSize,
26
+ gap,
27
+ visible,
28
+ initialIndex,
29
+ onOpenAt,
30
+ onClose,
31
+ style,
32
+ }: ProfileSelfiesGalleryViewProps) => {
33
+ const { theme, sizes } = useTheme();
34
+ const spacing = gap ?? ((sizes.sm as number) * 1.25);
35
+
36
+ const styles = useMemo(() => getStyles(theme, sizes, spacing), [theme, sizes, spacing]);
37
+
38
+ return (
39
+ <View style={[styles.wrapper, style]}>
40
+ <View style={styles.grid}>
41
+ {photos.map((photo, index) => {
42
+ const isLastInRow = (index + 1) % columns === 0;
43
+
44
+ return (
45
+ <Pressable
46
+ key={`${photo}-${index}`}
47
+ onPress={() => onOpenAt(index)}
48
+ android_ripple={{ color: "#00000022" }}
49
+ style={[
50
+ styles.imageWrapper,
51
+ { width: itemSize, height: itemSize, marginRight: isLastInRow ? 0 : spacing },
52
+ ]}
53
+ >
54
+ <Image source={{ uri: photo }} style={styles.image} resizeMode="cover" />
55
+ </Pressable>
56
+ );
57
+ })}
58
+ </View>
59
+
60
+ <GalleryModal
61
+ visible={visible}
62
+ images={photos}
63
+ initialIndex={initialIndex}
64
+ onRequestClose={onClose}
65
+ />
66
+ </View>
67
+ );
68
+ }
69
+ );
70
+
71
+ ProfileSelfiesGalleryView.displayName = "ProfileSelfiesGalleryView";
72
+
73
+ const getStyles = (theme: ThemeType, sizes: SizesType, spacing: number) =>
74
+ StyleSheet.create({
75
+ wrapper: {
76
+ backgroundColor: theme.card,
77
+ borderRadius: (sizes.radius_lg as number) + 4,
78
+ padding: sizes.lg as number,
79
+ },
80
+ grid: {
81
+ flexDirection: "row",
82
+ flexWrap: "wrap",
83
+ justifyContent: "flex-start",
84
+ },
85
+ imageWrapper: {
86
+ borderRadius: (sizes.radius_lg as number) + 2,
87
+ overflow: "hidden",
88
+ marginBottom: spacing,
89
+ backgroundColor: theme.backgroundSecond,
90
+ },
91
+ image: {
92
+ width: "100%",
93
+ height: "100%",
94
+ },
95
+ });
96
+
97
+ export default ProfileSelfiesGalleryView;
@@ -0,0 +1,86 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import { Meta, StoryFn } from '@storybook/react';
3
+ import { Dimensions, View } from 'react-native';
4
+ import { ThemeProvider } from '../../theme';
5
+ import { ProfileSelfiesGalleryView } from './ProfileSelfiesGallery.pure';
6
+
7
+ type Props = React.ComponentProps<typeof ProfileSelfiesGalleryView>;
8
+
9
+ const WINDOW_WIDTH = Dimensions.get('window').width;
10
+
11
+ const meta: Meta<Props> = {
12
+ title: 'Features/Gallery/ProfileSelfiesGallery',
13
+ component: ProfileSelfiesGalleryView,
14
+ decorators: [
15
+ (Story) => (
16
+ <ThemeProvider>
17
+ <View style={{ paddingVertical: 24, paddingHorizontal: 16 }}>
18
+ <Story />
19
+ </View>
20
+ </ThemeProvider>
21
+ ),
22
+ ],
23
+ args: {
24
+ columns: 2,
25
+ },
26
+ };
27
+
28
+ export default meta;
29
+
30
+ const Template: StoryFn<Props> = ({
31
+ visible: _visible,
32
+ initialIndex: _initialIndex,
33
+ onOpenAt: _onOpenAt,
34
+ onClose: _onClose,
35
+ itemSize: _itemSize,
36
+ ...rest
37
+ }) => {
38
+ const [visible, setVisible] = useState(false);
39
+ const [initialIndex, setInitialIndex] = useState(0);
40
+
41
+ const itemSize = useMemo(() => {
42
+ const decoratorPadding = 32; // paddingHorizontal from decorator View (16 * 2)
43
+ const wrapperPadding = 40; // padding from component wrapper (20 * 2)
44
+ const gap = rest.gap ?? 15; // default spacing from component
45
+ const columns = rest.columns ?? 2;
46
+
47
+ return Math.floor((WINDOW_WIDTH - decoratorPadding - wrapperPadding - gap * (columns - 1)) / columns);
48
+ }, [rest.columns, rest.gap]);
49
+
50
+ return (
51
+ <ProfileSelfiesGalleryView
52
+ {...rest}
53
+ itemSize={itemSize}
54
+ visible={visible}
55
+ initialIndex={initialIndex}
56
+ onOpenAt={(index) => {
57
+ setInitialIndex(index);
58
+ setVisible(true);
59
+ }}
60
+ onClose={() => setVisible(false)}
61
+ />
62
+ );
63
+ };
64
+
65
+ export const Default = Template.bind({});
66
+ Default.args = {
67
+ photos: [
68
+ 'https://images.unsplash.com/photo-1690733583113-10a2dbd4c3fb?auto=format&fit=crop&w=800&q=80',
69
+ 'https://images.unsplash.com/photo-1633332755192-727a05c4013d?auto=format&fit=crop&w=800&q=80',
70
+ 'https://images.unsplash.com/photo-1544723795-3fb6469f5b39?auto=format&fit=crop&w=800&q=80',
71
+ 'https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=crop&w=800&q=80',
72
+ 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=800&q=80',
73
+ 'https://images.unsplash.com/photo-1520813792240-56fc4a3765a7?auto=format&fit=crop&w=800&q=80',
74
+ ],
75
+ };
76
+
77
+ export const WithCustomGap = Template.bind({});
78
+ WithCustomGap.args = {
79
+ gap: 12,
80
+ photos: [
81
+ 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&w=800&q=80',
82
+ 'https://images.unsplash.com/photo-1531256456869-ce942a665e80?auto=format&fit=crop&w=800&q=80',
83
+ 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=800&q=80',
84
+ 'https://images.unsplash.com/photo-1531891437562-4301cf35b7e4?auto=format&fit=crop&w=800&q=80',
85
+ ],
86
+ };