@idealyst/cli 1.0.41 → 1.0.43
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.
- package/dist/generators/api.js +1 -2
- package/dist/generators/api.js.map +1 -1
- package/dist/generators/database.js +1 -2
- package/dist/generators/database.js.map +1 -1
- package/dist/generators/fullstack.js +371 -0
- package/dist/generators/fullstack.js.map +1 -0
- package/dist/generators/index.js +5 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/native.js +2 -1
- package/dist/generators/native.js.map +1 -1
- package/dist/generators/shared.js +1 -2
- package/dist/generators/shared.js.map +1 -1
- package/dist/generators/utils.js +71 -7
- package/dist/generators/utils.js.map +1 -1
- package/dist/generators/web.js +1 -2
- package/dist/generators/web.js.map +1 -1
- package/dist/generators/workspace.js +56 -2
- package/dist/generators/workspace.js.map +1 -1
- package/dist/index.js +50 -2
- package/dist/index.js.map +1 -1
- package/dist/templates/database/.env.example +1 -8
- package/dist/templates/database/README.md +29 -74
- package/dist/templates/database/package.json +20 -34
- package/dist/templates/database/prisma/seed.ts +11 -11
- package/dist/templates/database/schema.prisma +97 -0
- package/dist/templates/database/src/index.ts +12 -8
- package/dist/templates/database/tsconfig.json +9 -23
- package/dist/templates/native/src/App-with-trpc-and-shared.tsx +266 -0
- package/dist/templates/shared/package.json +28 -3
- package/dist/templates/shared/src/components/index.ts +392 -0
- package/dist/templates/shared/src/index.ts +59 -1
- package/dist/templates/shared/src/types/index.ts +148 -0
- package/dist/templates/shared/src/utils/index.ts +278 -0
- package/dist/templates/web/package.json +2 -2
- package/dist/templates/web/src/App-with-trpc-and-shared.tsx +304 -0
- package/dist/templates/workspace/.devcontainer/Dockerfile +1 -1
- package/dist/templates/workspace/.devcontainer/devcontainer.json +7 -2
- package/dist/templates/workspace/.devcontainer/docker-compose.yml +14 -0
- package/dist/templates/workspace/.devcontainer/figma-mcp.sh +32 -0
- package/dist/templates/workspace/.devcontainer/setup.sh +3 -0
- package/dist/templates/workspace/setup.sh +22 -197
- package/dist/templates/workspace/tsconfig.json +32 -0
- package/dist/types/generators/fullstack.d.ts +2 -0
- package/dist/types/generators/index.d.ts +1 -0
- package/dist/types/generators/utils.d.ts +4 -1
- package/dist/types/types.d.ts +3 -1
- package/package.json +1 -1
- package/templates/database/.env.example +1 -0
- package/templates/database/README.md +48 -0
- package/templates/database/package.json +21 -2
- package/templates/database/prisma/seed.ts +28 -0
- package/templates/database/schema.prisma +85 -9
- package/templates/database/src/index.ts +7 -7
- package/templates/database/src/validators.ts +10 -0
- package/templates/native/src/App-with-trpc-and-shared.tsx +266 -0
- package/templates/shared/package.json +28 -3
- package/templates/shared/src/components/index.ts +392 -0
- package/templates/shared/src/index.ts +59 -1
- package/templates/shared/src/types/index.ts +148 -0
- package/templates/shared/src/utils/index.ts +278 -0
- package/templates/web/package.json +1 -1
- package/templates/web/src/App-with-trpc-and-shared.tsx +304 -0
- package/templates/workspace/.devcontainer/devcontainer.json +7 -2
- package/templates/workspace/.devcontainer/docker-compose.yml +14 -0
- package/templates/workspace/.devcontainer/figma-mcp.sh +32 -0
- package/templates/workspace/.devcontainer/setup.sh +3 -0
- package/templates/workspace/setup.sh +30 -0
- package/templates/workspace/tsconfig.json +32 -0
- package/dist/templates/database/__tests__/database.test.ts +0 -14
- package/dist/templates/database/jest.config.js +0 -19
- package/dist/templates/database/jest.setup.js +0 -11
- package/dist/templates/database/prisma/schema.prisma +0 -21
- package/dist/templates/database/src/client.ts +0 -18
- package/dist/templates/database/src/schemas.ts +0 -26
- package/dist/templates/workspace/scripts/docker/db-backup.sh +0 -230
- package/dist/templates/workspace/scripts/docker/deploy.sh +0 -212
- package/dist/templates/workspace/scripts/test-runner.js +0 -120
- /package/{templates/database/src/validatgors.ts → dist/templates/database/src/validators.ts} +0 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
+
import { httpBatchLink } from '@trpc/client';
|
|
4
|
+
import { Screen, Text, View, Button, ScrollView } from '@idealyst/components';
|
|
5
|
+
import { trpc } from './utils/trpc';
|
|
6
|
+
|
|
7
|
+
// Import shared components and utilities
|
|
8
|
+
import {
|
|
9
|
+
UserCard,
|
|
10
|
+
PostCard,
|
|
11
|
+
LoadingSpinner,
|
|
12
|
+
ErrorMessage,
|
|
13
|
+
FeatureCard,
|
|
14
|
+
TabButton,
|
|
15
|
+
DEMO_USERS,
|
|
16
|
+
DEMO_POSTS,
|
|
17
|
+
formatRelativeTime,
|
|
18
|
+
type User,
|
|
19
|
+
type Post
|
|
20
|
+
} from '{{workspaceScope}}/shared';
|
|
21
|
+
|
|
22
|
+
// Create tRPC client
|
|
23
|
+
const queryClient = new QueryClient();
|
|
24
|
+
|
|
25
|
+
const trpcClient = trpc.createClient({
|
|
26
|
+
links: [
|
|
27
|
+
httpBatchLink({
|
|
28
|
+
url: 'http://localhost:3001/trpc', // Update this to your API URL
|
|
29
|
+
// For device testing, you might need: 'http://192.168.1.xxx:3001/trpc'
|
|
30
|
+
// Optional: Add headers for authentication
|
|
31
|
+
// headers() {
|
|
32
|
+
// return {
|
|
33
|
+
// authorization: getAuthToken(),
|
|
34
|
+
// };
|
|
35
|
+
// },
|
|
36
|
+
}),
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function App() {
|
|
41
|
+
const [currentTab, setCurrentTab] = useState<'home' | 'users' | 'posts'>('home');
|
|
42
|
+
|
|
43
|
+
// Example tRPC usage
|
|
44
|
+
const { data: helloData, isLoading: helloLoading, error: helloError } = trpc.hello.useQuery({ name: 'Mobile User' });
|
|
45
|
+
const { data: usersData, isLoading: usersLoading } = trpc.users.getAll.useQuery();
|
|
46
|
+
const { data: postsData, isLoading: postsLoading } = trpc.posts.getAll.useQuery();
|
|
47
|
+
|
|
48
|
+
const renderHome = () => (
|
|
49
|
+
<ScrollView style={{ flex: 1 }}>
|
|
50
|
+
<View style={{ padding: 20 }}>
|
|
51
|
+
{/* Welcome Section */}
|
|
52
|
+
<View style={{ marginBottom: 32, alignItems: 'center' }}>
|
|
53
|
+
<Text variant="h1" style={{ textAlign: 'center', marginBottom: 16 }}>
|
|
54
|
+
Welcome to {{appName}}! 📱
|
|
55
|
+
</Text>
|
|
56
|
+
<Text variant="body" style={{ textAlign: 'center', marginBottom: 16, fontSize: 18 }}>
|
|
57
|
+
A cross-platform mobile app built with React Native and the Idealyst Framework
|
|
58
|
+
</Text>
|
|
59
|
+
|
|
60
|
+
{/* tRPC Connection Test */}
|
|
61
|
+
<View style={{
|
|
62
|
+
padding: 16,
|
|
63
|
+
backgroundColor: '#e3f2fd',
|
|
64
|
+
borderRadius: 8,
|
|
65
|
+
marginBottom: 24,
|
|
66
|
+
width: '100%'
|
|
67
|
+
}}>
|
|
68
|
+
<Text variant="h3" style={{ marginBottom: 8, textAlign: 'center' }}>🔗 API Connection</Text>
|
|
69
|
+
{helloLoading && <Text style={{ textAlign: 'center' }}>Testing connection...</Text>}
|
|
70
|
+
{helloError && <Text style={{ color: 'red', textAlign: 'center' }}>Error: {helloError.message}</Text>}
|
|
71
|
+
{helloData && <Text style={{ color: 'green', textAlign: 'center' }}>✅ {helloData.greeting}</Text>}
|
|
72
|
+
</View>
|
|
73
|
+
</View>
|
|
74
|
+
|
|
75
|
+
{/* Features Overview */}
|
|
76
|
+
<View style={{ marginBottom: 32 }}>
|
|
77
|
+
<Text variant="h2" style={{ marginBottom: 16, textAlign: 'center' }}>🏗️ What's Included</Text>
|
|
78
|
+
<FeatureCard
|
|
79
|
+
icon="🔗"
|
|
80
|
+
title="Full Integration"
|
|
81
|
+
description="Connected to your database and API with end-to-end type safety"
|
|
82
|
+
/>
|
|
83
|
+
<FeatureCard
|
|
84
|
+
icon="📦"
|
|
85
|
+
title="Shared Components"
|
|
86
|
+
description="Cross-platform UI components that work on web and mobile"
|
|
87
|
+
/>
|
|
88
|
+
<FeatureCard
|
|
89
|
+
icon="🎨"
|
|
90
|
+
title="Idealyst Design"
|
|
91
|
+
description="Beautiful, consistent styling with the Idealyst component library"
|
|
92
|
+
/>
|
|
93
|
+
<FeatureCard
|
|
94
|
+
icon="⚡"
|
|
95
|
+
title="Real-time Updates"
|
|
96
|
+
description="tRPC provides instant synchronization with your backend"
|
|
97
|
+
/>
|
|
98
|
+
</View>
|
|
99
|
+
|
|
100
|
+
{/* Quick Data Preview */}
|
|
101
|
+
<View style={{ marginBottom: 32 }}>
|
|
102
|
+
<Text variant="h2" style={{ marginBottom: 16, textAlign: 'center' }}>📊 Live Data</Text>
|
|
103
|
+
|
|
104
|
+
{/* Users Preview */}
|
|
105
|
+
<View style={{ marginBottom: 20 }}>
|
|
106
|
+
<Text variant="h3" style={{ marginBottom: 12 }}>👥 Users ({usersLoading ? '...' : usersData?.length || DEMO_USERS.length})</Text>
|
|
107
|
+
{usersLoading ? (
|
|
108
|
+
<LoadingSpinner message="Loading users..." />
|
|
109
|
+
) : (
|
|
110
|
+
<View>
|
|
111
|
+
{(usersData || DEMO_USERS.slice(0, 2)).map((user: User) => (
|
|
112
|
+
<UserCard
|
|
113
|
+
key={user.id}
|
|
114
|
+
user={user}
|
|
115
|
+
showBio={false}
|
|
116
|
+
onPress={() => console.log('View profile:', user.name)}
|
|
117
|
+
/>
|
|
118
|
+
))}
|
|
119
|
+
</View>
|
|
120
|
+
)}
|
|
121
|
+
</View>
|
|
122
|
+
|
|
123
|
+
{/* Posts Preview */}
|
|
124
|
+
<View style={{ marginBottom: 20 }}>
|
|
125
|
+
<Text variant="h3" style={{ marginBottom: 12 }}>📝 Recent Posts ({postsLoading ? '...' : postsData?.length || DEMO_POSTS.length})</Text>
|
|
126
|
+
{postsLoading ? (
|
|
127
|
+
<LoadingSpinner message="Loading posts..." />
|
|
128
|
+
) : (
|
|
129
|
+
<View>
|
|
130
|
+
{(postsData || DEMO_POSTS.slice(0, 1)).map((post: Post) => {
|
|
131
|
+
const author = DEMO_USERS.find(u => u.id === post.authorId);
|
|
132
|
+
return (
|
|
133
|
+
<PostCard
|
|
134
|
+
key={post.id}
|
|
135
|
+
post={post}
|
|
136
|
+
author={author}
|
|
137
|
+
onPress={() => console.log('Read post:', post.title)}
|
|
138
|
+
onLike={() => console.log('Like post:', post.title)}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
})}
|
|
142
|
+
</View>
|
|
143
|
+
)}
|
|
144
|
+
</View>
|
|
145
|
+
</View>
|
|
146
|
+
|
|
147
|
+
{/* Development Info */}
|
|
148
|
+
<View style={{
|
|
149
|
+
padding: 20,
|
|
150
|
+
backgroundColor: '#f8f9fa',
|
|
151
|
+
borderRadius: 8,
|
|
152
|
+
marginBottom: 24
|
|
153
|
+
}}>
|
|
154
|
+
<Text variant="h3" style={{ marginBottom: 12, textAlign: 'center' }}>🚀 Development</Text>
|
|
155
|
+
<Text variant="body" style={{ textAlign: 'center', marginBottom: 12 }}>
|
|
156
|
+
This app is part of your full-stack workspace. Make changes to see them reflected instantly!
|
|
157
|
+
</Text>
|
|
158
|
+
<Text variant="caption" style={{ textAlign: 'center', fontStyle: 'italic' }}>
|
|
159
|
+
Edit packages/mobile/src/App.tsx to customize this screen
|
|
160
|
+
</Text>
|
|
161
|
+
</View>
|
|
162
|
+
</View>
|
|
163
|
+
</ScrollView>
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const renderUsers = () => {
|
|
167
|
+
const { data: users, isLoading, error } = trpc.users.getAll.useQuery();
|
|
168
|
+
|
|
169
|
+
if (isLoading) return <LoadingSpinner message="Loading users..." />;
|
|
170
|
+
if (error) return <ErrorMessage message={error.message} />;
|
|
171
|
+
|
|
172
|
+
const allUsers = users || DEMO_USERS;
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<ScrollView style={{ flex: 1, padding: 20 }}>
|
|
176
|
+
<Text variant="h1" style={{ marginBottom: 20, textAlign: 'center' }}>👥 All Users ({allUsers.length})</Text>
|
|
177
|
+
<View>
|
|
178
|
+
{allUsers.map((user: User) => (
|
|
179
|
+
<UserCard
|
|
180
|
+
key={user.id}
|
|
181
|
+
user={user}
|
|
182
|
+
showBio={true}
|
|
183
|
+
onPress={() => console.log('View profile:', user.name)}
|
|
184
|
+
/>
|
|
185
|
+
))}
|
|
186
|
+
</View>
|
|
187
|
+
</ScrollView>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const renderPosts = () => {
|
|
192
|
+
const { data: posts, isLoading, error } = trpc.posts.getAll.useQuery();
|
|
193
|
+
|
|
194
|
+
if (isLoading) return <LoadingSpinner message="Loading posts..." />;
|
|
195
|
+
if (error) return <ErrorMessage message={error.message} />;
|
|
196
|
+
|
|
197
|
+
const allPosts = posts || DEMO_POSTS;
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<ScrollView style={{ flex: 1, padding: 20 }}>
|
|
201
|
+
<Text variant="h1" style={{ marginBottom: 20, textAlign: 'center' }}>📝 All Posts ({allPosts.length})</Text>
|
|
202
|
+
<View>
|
|
203
|
+
{allPosts.map((post: Post) => {
|
|
204
|
+
const author = DEMO_USERS.find(u => u.id === post.authorId);
|
|
205
|
+
return (
|
|
206
|
+
<PostCard
|
|
207
|
+
key={post.id}
|
|
208
|
+
post={post}
|
|
209
|
+
author={author}
|
|
210
|
+
showFullContent={false}
|
|
211
|
+
onPress={() => console.log('Read post:', post.title)}
|
|
212
|
+
onLike={() => console.log('Like post:', post.title)}
|
|
213
|
+
/>
|
|
214
|
+
);
|
|
215
|
+
})}
|
|
216
|
+
</View>
|
|
217
|
+
</ScrollView>
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const renderTabBar = () => (
|
|
222
|
+
<View style={{
|
|
223
|
+
flexDirection: 'row',
|
|
224
|
+
backgroundColor: '#f8f9fa',
|
|
225
|
+
borderTopWidth: 1,
|
|
226
|
+
borderTopColor: '#e9ecef',
|
|
227
|
+
paddingVertical: 10
|
|
228
|
+
}}>
|
|
229
|
+
<TabButton
|
|
230
|
+
title="Home"
|
|
231
|
+
icon="🏠"
|
|
232
|
+
active={currentTab === 'home'}
|
|
233
|
+
onPress={() => setCurrentTab('home')}
|
|
234
|
+
/>
|
|
235
|
+
<TabButton
|
|
236
|
+
title="Users"
|
|
237
|
+
icon="👥"
|
|
238
|
+
active={currentTab === 'users'}
|
|
239
|
+
onPress={() => setCurrentTab('users')}
|
|
240
|
+
/>
|
|
241
|
+
<TabButton
|
|
242
|
+
title="Posts"
|
|
243
|
+
icon="📝"
|
|
244
|
+
active={currentTab === 'posts'}
|
|
245
|
+
onPress={() => setCurrentTab('posts')}
|
|
246
|
+
/>
|
|
247
|
+
</View>
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
|
252
|
+
<QueryClientProvider client={queryClient}>
|
|
253
|
+
<Screen>
|
|
254
|
+
<View style={{ flex: 1 }}>
|
|
255
|
+
{currentTab === 'home' && renderHome()}
|
|
256
|
+
{currentTab === 'users' && renderUsers()}
|
|
257
|
+
{currentTab === 'posts' && renderPosts()}
|
|
258
|
+
{renderTabBar()}
|
|
259
|
+
</View>
|
|
260
|
+
</Screen>
|
|
261
|
+
</QueryClientProvider>
|
|
262
|
+
</trpc.Provider>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export default App;
|
|
@@ -2,9 +2,31 @@
|
|
|
2
2
|
"name": "{{packageName}}",
|
|
3
3
|
"version": "{{version}}",
|
|
4
4
|
"description": "{{description}}",
|
|
5
|
-
"main": "
|
|
6
|
-
"module": "
|
|
7
|
-
"types": "
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./types": {
|
|
15
|
+
"import": "./dist/types/index.js",
|
|
16
|
+
"require": "./dist/types/index.js",
|
|
17
|
+
"types": "./dist/types/index.d.ts"
|
|
18
|
+
},
|
|
19
|
+
"./components": {
|
|
20
|
+
"import": "./dist/components/index.js",
|
|
21
|
+
"require": "./dist/components/index.js",
|
|
22
|
+
"types": "./dist/components/index.d.ts"
|
|
23
|
+
},
|
|
24
|
+
"./utils": {
|
|
25
|
+
"import": "./dist/utils/index.js",
|
|
26
|
+
"require": "./dist/utils/index.js",
|
|
27
|
+
"types": "./dist/utils/index.d.ts"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
8
30
|
"scripts": {
|
|
9
31
|
"build": "tsc",
|
|
10
32
|
"test": "jest",
|
|
@@ -12,6 +34,9 @@
|
|
|
12
34
|
"test:coverage": "jest --coverage",
|
|
13
35
|
"type-check": "tsc --noEmit"
|
|
14
36
|
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"zod": "^3.22.4"
|
|
39
|
+
},
|
|
15
40
|
"peerDependencies": {
|
|
16
41
|
"@idealyst/components": "^1.0.21",
|
|
17
42
|
"@idealyst/navigation": "^1.0.21",
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, Button, Image } from '@idealyst/components';
|
|
3
|
+
import type { User, Post, Comment } from '../types';
|
|
4
|
+
|
|
5
|
+
interface UserCardProps {
|
|
6
|
+
user: User;
|
|
7
|
+
onPress?: () => void;
|
|
8
|
+
showBio?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const UserCard: React.FC<UserCardProps> = ({
|
|
12
|
+
user,
|
|
13
|
+
onPress,
|
|
14
|
+
showBio = false
|
|
15
|
+
}) => {
|
|
16
|
+
return (
|
|
17
|
+
<View
|
|
18
|
+
style={{
|
|
19
|
+
padding: 16,
|
|
20
|
+
borderRadius: 8,
|
|
21
|
+
backgroundColor: 'white',
|
|
22
|
+
shadowColor: '#000',
|
|
23
|
+
shadowOffset: { width: 0, height: 2 },
|
|
24
|
+
shadowOpacity: 0.1,
|
|
25
|
+
shadowRadius: 4,
|
|
26
|
+
elevation: 2,
|
|
27
|
+
marginBottom: 8
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
31
|
+
{user.avatar && (
|
|
32
|
+
<Image
|
|
33
|
+
source={{ uri: user.avatar }}
|
|
34
|
+
style={{
|
|
35
|
+
width: 50,
|
|
36
|
+
height: 50,
|
|
37
|
+
borderRadius: 25,
|
|
38
|
+
marginRight: 12
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
)}
|
|
42
|
+
<View style={{ flex: 1 }}>
|
|
43
|
+
<Text variant="h3" style={{ marginBottom: 4 }}>
|
|
44
|
+
{user.name || 'Anonymous User'}
|
|
45
|
+
</Text>
|
|
46
|
+
<Text variant="caption" style={{ color: 'gray' }}>
|
|
47
|
+
{user.email}
|
|
48
|
+
</Text>
|
|
49
|
+
{user.location && (
|
|
50
|
+
<Text variant="caption" style={{ color: 'gray', marginTop: 2 }}>
|
|
51
|
+
📍 {user.location}
|
|
52
|
+
</Text>
|
|
53
|
+
)}
|
|
54
|
+
</View>
|
|
55
|
+
{onPress && (
|
|
56
|
+
<Button
|
|
57
|
+
title="View Profile"
|
|
58
|
+
onPress={onPress}
|
|
59
|
+
size="small"
|
|
60
|
+
/>
|
|
61
|
+
)}
|
|
62
|
+
</View>
|
|
63
|
+
|
|
64
|
+
{showBio && user.bio && (
|
|
65
|
+
<Text variant="body" style={{ marginTop: 12, fontStyle: 'italic' }}>
|
|
66
|
+
{user.bio}
|
|
67
|
+
</Text>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
{user.website && (
|
|
71
|
+
<Text variant="caption" style={{ marginTop: 8, color: 'blue' }}>
|
|
72
|
+
🌐 {user.website}
|
|
73
|
+
</Text>
|
|
74
|
+
)}
|
|
75
|
+
</View>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
interface PostCardProps {
|
|
80
|
+
post: Post;
|
|
81
|
+
author?: User;
|
|
82
|
+
onPress?: () => void;
|
|
83
|
+
onLike?: () => void;
|
|
84
|
+
showFullContent?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const PostCard: React.FC<PostCardProps> = ({
|
|
88
|
+
post,
|
|
89
|
+
author,
|
|
90
|
+
onPress,
|
|
91
|
+
onLike,
|
|
92
|
+
showFullContent = false
|
|
93
|
+
}) => {
|
|
94
|
+
const content = showFullContent ? post.content : (post.excerpt || post.content.substring(0, 150) + '...');
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<View
|
|
98
|
+
style={{
|
|
99
|
+
padding: 16,
|
|
100
|
+
borderRadius: 8,
|
|
101
|
+
backgroundColor: 'white',
|
|
102
|
+
shadowColor: '#000',
|
|
103
|
+
shadowOffset: { width: 0, height: 2 },
|
|
104
|
+
shadowOpacity: 0.1,
|
|
105
|
+
shadowRadius: 4,
|
|
106
|
+
elevation: 2,
|
|
107
|
+
marginBottom: 12
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{/* Post Header */}
|
|
111
|
+
{author && (
|
|
112
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12 }}>
|
|
113
|
+
{author.avatar && (
|
|
114
|
+
<Image
|
|
115
|
+
source={{ uri: author.avatar }}
|
|
116
|
+
style={{
|
|
117
|
+
width: 32,
|
|
118
|
+
height: 32,
|
|
119
|
+
borderRadius: 16,
|
|
120
|
+
marginRight: 8
|
|
121
|
+
}}
|
|
122
|
+
/>
|
|
123
|
+
)}
|
|
124
|
+
<View>
|
|
125
|
+
<Text variant="body" style={{ fontWeight: 'bold' }}>
|
|
126
|
+
{author.name || 'Anonymous'}
|
|
127
|
+
</Text>
|
|
128
|
+
<Text variant="caption" style={{ color: 'gray' }}>
|
|
129
|
+
{new Date(post.createdAt).toLocaleDateString()}
|
|
130
|
+
</Text>
|
|
131
|
+
</View>
|
|
132
|
+
</View>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{/* Post Content */}
|
|
136
|
+
<Text variant="h2" style={{ marginBottom: 8 }}>
|
|
137
|
+
{post.title}
|
|
138
|
+
</Text>
|
|
139
|
+
|
|
140
|
+
<Text variant="body" style={{ marginBottom: 12 }}>
|
|
141
|
+
{content}
|
|
142
|
+
</Text>
|
|
143
|
+
|
|
144
|
+
{/* Tags */}
|
|
145
|
+
{post.tags.length > 0 && (
|
|
146
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', marginBottom: 12 }}>
|
|
147
|
+
{post.tags.map((tag, index) => (
|
|
148
|
+
<View
|
|
149
|
+
key={index}
|
|
150
|
+
style={{
|
|
151
|
+
backgroundColor: '#e3f2fd',
|
|
152
|
+
paddingHorizontal: 8,
|
|
153
|
+
paddingVertical: 4,
|
|
154
|
+
borderRadius: 12,
|
|
155
|
+
marginRight: 6,
|
|
156
|
+
marginBottom: 4
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<Text variant="caption" style={{ color: '#1976d2' }}>
|
|
160
|
+
#{tag}
|
|
161
|
+
</Text>
|
|
162
|
+
</View>
|
|
163
|
+
))}
|
|
164
|
+
</View>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
{/* Post Stats and Actions */}
|
|
168
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
169
|
+
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
170
|
+
<Text variant="caption" style={{ color: 'gray', marginRight: 12 }}>
|
|
171
|
+
👁️ {post.views} views
|
|
172
|
+
</Text>
|
|
173
|
+
<Text variant="caption" style={{ color: 'gray' }}>
|
|
174
|
+
❤️ {post.likes} likes
|
|
175
|
+
</Text>
|
|
176
|
+
</View>
|
|
177
|
+
|
|
178
|
+
<View style={{ flexDirection: 'row' }}>
|
|
179
|
+
{onLike && (
|
|
180
|
+
<Button
|
|
181
|
+
title="Like"
|
|
182
|
+
onPress={onLike}
|
|
183
|
+
size="small"
|
|
184
|
+
variant="outline"
|
|
185
|
+
style={{ marginRight: 8 }}
|
|
186
|
+
/>
|
|
187
|
+
)}
|
|
188
|
+
{onPress && (
|
|
189
|
+
<Button
|
|
190
|
+
title="Read More"
|
|
191
|
+
onPress={onPress}
|
|
192
|
+
size="small"
|
|
193
|
+
/>
|
|
194
|
+
)}
|
|
195
|
+
</View>
|
|
196
|
+
</View>
|
|
197
|
+
</View>
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
interface CommentCardProps {
|
|
202
|
+
comment: Comment;
|
|
203
|
+
author?: User;
|
|
204
|
+
onReply?: () => void;
|
|
205
|
+
level?: number;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export const CommentCard: React.FC<CommentCardProps> = ({
|
|
209
|
+
comment,
|
|
210
|
+
author,
|
|
211
|
+
onReply,
|
|
212
|
+
level = 0
|
|
213
|
+
}) => {
|
|
214
|
+
const indentStyle = {
|
|
215
|
+
marginLeft: level * 20,
|
|
216
|
+
maxWidth: level > 2 ? '90%' : '100%'
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<View
|
|
221
|
+
style={[
|
|
222
|
+
{
|
|
223
|
+
padding: 12,
|
|
224
|
+
borderRadius: 6,
|
|
225
|
+
backgroundColor: level === 0 ? 'white' : '#f8f9fa',
|
|
226
|
+
borderLeftWidth: level > 0 ? 3 : 0,
|
|
227
|
+
borderLeftColor: '#e3f2fd',
|
|
228
|
+
marginBottom: 8
|
|
229
|
+
},
|
|
230
|
+
indentStyle
|
|
231
|
+
]}
|
|
232
|
+
>
|
|
233
|
+
{/* Comment Header */}
|
|
234
|
+
{author && (
|
|
235
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 8 }}>
|
|
236
|
+
{author.avatar && (
|
|
237
|
+
<Image
|
|
238
|
+
source={{ uri: author.avatar }}
|
|
239
|
+
style={{
|
|
240
|
+
width: 24,
|
|
241
|
+
height: 24,
|
|
242
|
+
borderRadius: 12,
|
|
243
|
+
marginRight: 6
|
|
244
|
+
}}
|
|
245
|
+
/>
|
|
246
|
+
)}
|
|
247
|
+
<Text variant="body" style={{ fontWeight: 'bold', fontSize: 14 }}>
|
|
248
|
+
{author.name || 'Anonymous'}
|
|
249
|
+
</Text>
|
|
250
|
+
<Text variant="caption" style={{ color: 'gray', marginLeft: 8 }}>
|
|
251
|
+
{new Date(comment.createdAt).toLocaleDateString()}
|
|
252
|
+
</Text>
|
|
253
|
+
</View>
|
|
254
|
+
)}
|
|
255
|
+
|
|
256
|
+
{/* Comment Content */}
|
|
257
|
+
<Text variant="body" style={{ marginBottom: 8 }}>
|
|
258
|
+
{comment.content}
|
|
259
|
+
</Text>
|
|
260
|
+
|
|
261
|
+
{/* Comment Actions */}
|
|
262
|
+
{onReply && level < 3 && (
|
|
263
|
+
<Button
|
|
264
|
+
title="Reply"
|
|
265
|
+
onPress={onReply}
|
|
266
|
+
size="small"
|
|
267
|
+
variant="outline"
|
|
268
|
+
/>
|
|
269
|
+
)}
|
|
270
|
+
</View>
|
|
271
|
+
);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
interface LoadingSpinnerProps {
|
|
275
|
+
size?: 'small' | 'medium' | 'large';
|
|
276
|
+
message?: string;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
|
|
280
|
+
size = 'medium',
|
|
281
|
+
message
|
|
282
|
+
}) => {
|
|
283
|
+
const sizeMap = {
|
|
284
|
+
small: 20,
|
|
285
|
+
medium: 40,
|
|
286
|
+
large: 60
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<View style={{ alignItems: 'center', justifyContent: 'center', padding: 20 }}>
|
|
291
|
+
<View
|
|
292
|
+
style={{
|
|
293
|
+
width: sizeMap[size],
|
|
294
|
+
height: sizeMap[size],
|
|
295
|
+
borderRadius: sizeMap[size] / 2,
|
|
296
|
+
borderWidth: 3,
|
|
297
|
+
borderColor: '#e3f2fd',
|
|
298
|
+
borderTopColor: '#1976d2',
|
|
299
|
+
// Animation would be handled by the platform-specific implementation
|
|
300
|
+
}}
|
|
301
|
+
/>
|
|
302
|
+
{message && (
|
|
303
|
+
<Text variant="body" style={{ marginTop: 12, textAlign: 'center' }}>
|
|
304
|
+
{message}
|
|
305
|
+
</Text>
|
|
306
|
+
)}
|
|
307
|
+
</View>
|
|
308
|
+
);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
interface ErrorMessageProps {
|
|
312
|
+
message: string;
|
|
313
|
+
onRetry?: () => void;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export const ErrorMessage: React.FC<ErrorMessageProps> = ({ message, onRetry }) => {
|
|
317
|
+
return (
|
|
318
|
+
<View
|
|
319
|
+
style={{
|
|
320
|
+
padding: 16,
|
|
321
|
+
backgroundColor: '#ffebee',
|
|
322
|
+
borderRadius: 8,
|
|
323
|
+
borderLeftWidth: 4,
|
|
324
|
+
borderLeftColor: '#f44336',
|
|
325
|
+
margin: 16
|
|
326
|
+
}}
|
|
327
|
+
>
|
|
328
|
+
<Text variant="body" style={{ color: '#c62828', marginBottom: onRetry ? 12 : 0 }}>
|
|
329
|
+
⚠️ {message}
|
|
330
|
+
</Text>
|
|
331
|
+
{onRetry && (
|
|
332
|
+
<Button
|
|
333
|
+
title="Try Again"
|
|
334
|
+
onPress={onRetry}
|
|
335
|
+
size="small"
|
|
336
|
+
style={{ backgroundColor: '#f44336' }}
|
|
337
|
+
/>
|
|
338
|
+
)}
|
|
339
|
+
</View>
|
|
340
|
+
);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// Feature Card Component - reusable for both web and native
|
|
344
|
+
interface FeatureCardProps {
|
|
345
|
+
icon: string;
|
|
346
|
+
title: string;
|
|
347
|
+
description: string;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export const FeatureCard: React.FC<FeatureCardProps> = ({ icon, title, description }) => {
|
|
351
|
+
return (
|
|
352
|
+
<View style={{
|
|
353
|
+
padding: 16,
|
|
354
|
+
backgroundColor: 'white',
|
|
355
|
+
borderRadius: 8,
|
|
356
|
+
shadowColor: '#000',
|
|
357
|
+
shadowOffset: { width: 0, height: 2 },
|
|
358
|
+
shadowOpacity: 0.1,
|
|
359
|
+
shadowRadius: 4,
|
|
360
|
+
elevation: 2,
|
|
361
|
+
marginBottom: 12
|
|
362
|
+
}}>
|
|
363
|
+
<Text style={{ fontSize: 24, marginBottom: 8, textAlign: 'center' }}>{icon}</Text>
|
|
364
|
+
<Text variant="h4" style={{ marginBottom: 8, textAlign: 'center' }}>{title}</Text>
|
|
365
|
+
<Text variant="body" style={{ color: '#666', textAlign: 'center' }}>{description}</Text>
|
|
366
|
+
</View>
|
|
367
|
+
);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// Tab Button Component - for navigation tabs in mobile/web apps
|
|
371
|
+
interface TabButtonProps {
|
|
372
|
+
title: string;
|
|
373
|
+
icon: string;
|
|
374
|
+
active: boolean;
|
|
375
|
+
onPress: () => void;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export const TabButton: React.FC<TabButtonProps> = ({ title, icon, active, onPress }) => {
|
|
379
|
+
return (
|
|
380
|
+
<Button
|
|
381
|
+
title={`${icon} ${title}`}
|
|
382
|
+
onPress={onPress}
|
|
383
|
+
variant={active ? 'primary' : 'outline'}
|
|
384
|
+
style={{
|
|
385
|
+
flex: 1,
|
|
386
|
+
marginHorizontal: 4,
|
|
387
|
+
backgroundColor: active ? '#007bff' : 'transparent',
|
|
388
|
+
borderColor: active ? '#007bff' : '#ccc'
|
|
389
|
+
}}
|
|
390
|
+
/>
|
|
391
|
+
);
|
|
392
|
+
};
|