@mereb/app-feed 0.0.2 → 0.0.3

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.
@@ -1,6 +1,22 @@
1
1
  import type { FeedQuery } from '@mereb/shared-graphql';
2
2
  export { FeedDocument } from '@mereb/shared-graphql';
3
3
  export type FeedNode = FeedQuery['feedHome']['edges'][number]['node'];
4
+ export type CreatePostResponse = {
5
+ createPost: {
6
+ id: string;
7
+ body: string;
8
+ createdAt: string;
9
+ author: {
10
+ id: string;
11
+ handle: string;
12
+ };
13
+ };
14
+ };
15
+ export type CreatePostVariables = {
16
+ body: string;
17
+ mediaKeys?: string[];
18
+ };
19
+ export declare const CreatePostDocument: import("@apollo/client").DocumentNode;
4
20
  export declare const FEED_PAGE_SIZE = 20;
5
21
  export declare function parseFeedTimestamp(value?: string | null): Date | null;
6
22
  export declare function formatRelativeFeedTimestamp(value?: string | null, now?: number): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mereb/app-feed",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Feed experience building blocks for Mereb apps",
5
5
  "type": "module",
6
6
  "main": "src/index.tsx",
package/src/headless.ts CHANGED
@@ -1,7 +1,37 @@
1
+ import { gql } from '@apollo/client';
1
2
  import type { FeedQuery } from '@mereb/shared-graphql';
2
3
  export { FeedDocument } from '@mereb/shared-graphql';
3
4
 
4
5
  export type FeedNode = FeedQuery['feedHome']['edges'][number]['node'];
6
+ export type CreatePostResponse = {
7
+ createPost: {
8
+ id: string;
9
+ body: string;
10
+ createdAt: string;
11
+ author: {
12
+ id: string;
13
+ handle: string;
14
+ };
15
+ };
16
+ };
17
+ export type CreatePostVariables = {
18
+ body: string;
19
+ mediaKeys?: string[];
20
+ };
21
+
22
+ export const CreatePostDocument = gql`
23
+ mutation CreatePost($body: String!, $mediaKeys: [String!]) {
24
+ createPost(body: $body, mediaKeys: $mediaKeys) {
25
+ id
26
+ body
27
+ createdAt
28
+ author {
29
+ id
30
+ handle
31
+ }
32
+ }
33
+ }
34
+ `;
5
35
 
6
36
  export const FEED_PAGE_SIZE = 20;
7
37
 
@@ -42,7 +72,7 @@ export function formatRelativeFeedTimestamp(value?: string | null, now: number =
42
72
  }
43
73
 
44
74
  return date.toLocaleDateString(undefined, {
45
- year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined,
75
+ year: date.getFullYear() === new Date().getFullYear() ? undefined : 'numeric',
46
76
  month: 'short',
47
77
  day: 'numeric'
48
78
  });
package/src/index.tsx CHANGED
@@ -1,155 +1,157 @@
1
- import { useCallback, useMemo } from 'react';
2
- import {
3
- FlatList,
4
- RefreshControl,
5
- StyleSheet,
6
- Text,
7
- TouchableOpacity,
8
- View
9
- } from 'react-native';
10
- import { useFeedQuery } from '@mereb/shared-graphql';
11
- import { tokens } from '@mereb/tokens/native';
12
- import { cardRecipes } from '@mereb/ui-shared';
13
- import {
14
- formatRelativeFeedTimestamp,
15
- selectFeedNodes,
16
- type FeedNode
17
- } from './headless.js';
1
+ import {useCallback, useMemo} from 'react';
2
+ import {FlatList, RefreshControl, StyleSheet, Text, TouchableOpacity, View, type ViewStyle} from 'react-native';
3
+ import {useFeedQuery} from '@mereb/shared-graphql';
4
+ import {tokens} from '@mereb/tokens/native';
5
+ import {cardRecipes} from '@mereb/ui-shared';
6
+ import {type FeedNode, formatRelativeFeedTimestamp, selectFeedNodes} from './headless.js';
7
+
8
+ type SeparatorProps = {
9
+ style?: ViewStyle;
10
+ };
11
+
12
+ function Separator({style}: Readonly<SeparatorProps>) {
13
+ return <View style={style}/>;
14
+ }
15
+
16
+ /* Top-level wrapper so FlatList receives a stable component reference */
17
+ function SeparatorWithStyle() {
18
+ return <Separator style={styles.separator} />;
19
+ }
18
20
 
19
21
  type PostCardProps = {
20
- post: FeedNode;
21
- onSelectAuthor?: (handle: string) => void;
22
+ post: FeedNode;
23
+ onSelectAuthor?: (handle: string) => void;
22
24
  };
23
25
 
24
- function PostCard({ post, onSelectAuthor }: Readonly<PostCardProps>) {
25
- const handlePress = useCallback(() => {
26
- onSelectAuthor?.(post.author.handle);
27
- }, [onSelectAuthor, post.author.handle]);
26
+ function PostCard({post, onSelectAuthor}: Readonly<PostCardProps>) {
27
+ const handlePress = useCallback(() => {
28
+ onSelectAuthor?.(post.author.handle);
29
+ }, [onSelectAuthor, post.author.handle]);
28
30
 
29
- return (
30
- <View style={styles.card}>
31
- <View style={styles.metaRow}>
32
- {onSelectAuthor ? (
33
- <TouchableOpacity onPress={handlePress} accessibilityRole="button">
34
- <Text style={styles.handleLink}>@{post.author.handle}</Text>
35
- </TouchableOpacity>
36
- ) : (
37
- <Text style={styles.handle}>@{post.author.handle}</Text>
38
- )}
39
- <Text style={styles.metaSeparator}>•</Text>
40
- <Text style={styles.timestamp}>{formatRelativeFeedTimestamp(post.createdAt)}</Text>
41
- </View>
42
- <Text style={styles.body}>{post.body}</Text>
43
- </View>
44
- );
31
+ return (
32
+ <View style={styles.card}>
33
+ <View style={styles.metaRow}>
34
+ {onSelectAuthor ? (
35
+ <TouchableOpacity onPress={handlePress} accessibilityRole="button">
36
+ <Text style={styles.handleLink}>@{post.author.handle}</Text>
37
+ </TouchableOpacity>
38
+ ) : (
39
+ <Text style={styles.handle}>@{post.author.handle}</Text>
40
+ )}
41
+ <Text style={styles.metaSeparator}>•</Text>
42
+ <Text style={styles.timestamp}>{formatRelativeFeedTimestamp(post.createdAt)}</Text>
43
+ </View>
44
+ <Text style={styles.body}>{post.body}</Text>
45
+ </View>
46
+ );
45
47
  }
46
48
 
47
49
  type FeedScreenProps = {
48
- onSelectAuthor?: (handle: string) => void;
50
+ onSelectAuthor?: (handle: string) => void;
49
51
  };
50
52
 
51
- export function FeedScreen({ onSelectAuthor }: Readonly<FeedScreenProps>) {
52
- const { data, loading, error, refetch } = useFeedQuery();
53
+ export function FeedScreen({onSelectAuthor}: Readonly<FeedScreenProps>) {
54
+ const {data, loading, error, refetch} = useFeedQuery();
53
55
 
54
- const posts = useMemo(() => selectFeedNodes(data), [data]);
56
+ const posts = useMemo(() => selectFeedNodes(data), [data]);
55
57
 
56
- const handleRefresh = useCallback(() => {
57
- void refetch();
58
- }, [refetch]);
58
+ const handleRefresh = useCallback(() => {
59
+ void refetch();
60
+ }, [refetch]);
61
+
62
+ if (error) {
63
+ return (
64
+ <View style={styles.centered}>
65
+ <Text style={styles.errorText}>Failed to load feed</Text>
66
+ </View>
67
+ );
68
+ }
59
69
 
60
- if (error) {
61
70
  return (
62
- <View style={styles.centered}>
63
- <Text style={styles.errorText}>Failed to load feed</Text>
64
- </View>
71
+ <FlatList
72
+ data={posts}
73
+ keyExtractor={(item) => item.id}
74
+ contentContainerStyle={styles.listContent}
75
+ ItemSeparatorComponent={SeparatorWithStyle}
76
+ refreshControl={<RefreshControl refreshing={loading} onRefresh={handleRefresh}/>}
77
+ renderItem={({item}) => (
78
+ <PostCard post={item} onSelectAuthor={onSelectAuthor}/>
79
+ )}
80
+ ListEmptyComponent={
81
+ loading ? null : (
82
+ <View style={styles.centered}>
83
+ <Text style={styles.emptyTitle}>No posts yet</Text>
84
+ <Text style={styles.emptyBody}>
85
+ Pull down to refresh or check back later for new updates.
86
+ </Text>
87
+ </View>
88
+ )
89
+ }
90
+ />
65
91
  );
66
- }
67
-
68
- return (
69
- <FlatList
70
- data={posts}
71
- keyExtractor={(item) => item.id}
72
- contentContainerStyle={styles.listContent}
73
- ItemSeparatorComponent={() => <View style={styles.separator} />}
74
- refreshControl={<RefreshControl refreshing={loading} onRefresh={handleRefresh} />}
75
- renderItem={({ item }) => (
76
- <PostCard post={item} onSelectAuthor={onSelectAuthor} />
77
- )}
78
- ListEmptyComponent={
79
- !loading ? (
80
- <View style={styles.centered}>
81
- <Text style={styles.emptyTitle}>No posts yet</Text>
82
- <Text style={styles.emptyBody}>
83
- Pull down to refresh or check back later for new updates.
84
- </Text>
85
- </View>
86
- ) : null
87
- }
88
- />
89
- );
90
92
  }
91
93
 
92
- const { color, spacing, radius, shadow } = tokens;
94
+ const {color, spacing, radius, shadow} = tokens;
93
95
  const feedCardRecipe = cardRecipes.elevated;
94
96
 
95
97
  const styles = StyleSheet.create({
96
- listContent: {
97
- padding: spacing.lg,
98
- gap: spacing.md
99
- },
100
- separator: {
101
- height: 0
102
- },
103
- card: {
104
- backgroundColor: color[feedCardRecipe.backgroundColor],
105
- borderRadius: radius[feedCardRecipe.radius],
106
- padding: spacing[feedCardRecipe.padding],
107
- ...shadow[feedCardRecipe.elevation]
108
- },
109
- metaRow: {
110
- flexDirection: 'row',
111
- alignItems: 'center',
112
- marginBottom: spacing.sm
113
- },
114
- handle: {
115
- fontWeight: '600',
116
- color: color.text
117
- },
118
- handleLink: {
119
- fontWeight: '600',
120
- color: color.primary
121
- },
122
- metaSeparator: {
123
- marginHorizontal: spacing.xs,
124
- color: color.textMuted
125
- },
126
- timestamp: {
127
- color: color.textMuted
128
- },
129
- body: {
130
- fontSize: 16,
131
- lineHeight: 22,
132
- color: color.text
133
- },
134
- centered: {
135
- flex: 1,
136
- padding: spacing.xxxl,
137
- alignItems: 'center',
138
- justifyContent: 'center'
139
- },
140
- errorText: {
141
- fontSize: 16,
142
- color: '#d22c2c'
143
- },
144
- emptyTitle: {
145
- fontSize: 18,
146
- fontWeight: '600',
147
- color: color.text,
148
- marginBottom: spacing.sm
149
- },
150
- emptyBody: {
151
- fontSize: 14,
152
- color: color.textMuted,
153
- textAlign: 'center'
154
- }
98
+ listContent: {
99
+ padding: spacing.lg,
100
+ gap: spacing.md
101
+ },
102
+ separator: {
103
+ height: 0
104
+ },
105
+ card: {
106
+ backgroundColor: color[feedCardRecipe.backgroundColor],
107
+ borderRadius: radius[feedCardRecipe.radius],
108
+ padding: spacing[feedCardRecipe.padding],
109
+ ...shadow[feedCardRecipe.elevation]
110
+ },
111
+ metaRow: {
112
+ flexDirection: 'row',
113
+ alignItems: 'center',
114
+ marginBottom: spacing.sm
115
+ },
116
+ handle: {
117
+ fontWeight: '600',
118
+ color: color.text
119
+ },
120
+ handleLink: {
121
+ fontWeight: '600',
122
+ color: color.primary
123
+ },
124
+ metaSeparator: {
125
+ marginHorizontal: spacing.xs,
126
+ color: color.textMuted
127
+ },
128
+ timestamp: {
129
+ color: color.textMuted
130
+ },
131
+ body: {
132
+ fontSize: 16,
133
+ lineHeight: 22,
134
+ color: color.text
135
+ },
136
+ centered: {
137
+ flex: 1,
138
+ padding: spacing.xxxl,
139
+ alignItems: 'center',
140
+ justifyContent: 'center'
141
+ },
142
+ errorText: {
143
+ fontSize: 16,
144
+ color: '#d22c2c'
145
+ },
146
+ emptyTitle: {
147
+ fontSize: 18,
148
+ fontWeight: '600',
149
+ color: color.text,
150
+ marginBottom: spacing.sm
151
+ },
152
+ emptyBody: {
153
+ fontSize: 14,
154
+ color: color.textMuted,
155
+ textAlign: 'center'
156
+ }
155
157
  });