chore: vendor client into main repo
This commit is contained in:
50
client/app/(app)/_layout.tsx
Normal file
50
client/app/(app)/_layout.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { Pressable, Text } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRouter } from 'expo-router';
|
||||
|
||||
export default function AppLayout() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerTitleAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="home"
|
||||
options={{
|
||||
title: t('home.title'),
|
||||
headerRight: () => (
|
||||
<Pressable
|
||||
onPress={() => router.push('/(app)/settings')}
|
||||
hitSlop={10}
|
||||
>
|
||||
<Text>{t('home.settings')}</Text>
|
||||
</Pressable>
|
||||
),
|
||||
headerLeft: () => (
|
||||
<Pressable onPress={() => router.push('/(app)/favorites')} hitSlop={10}>
|
||||
<Text>{t('home.favorites')}</Text>
|
||||
</Pressable>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="favorites"
|
||||
options={{
|
||||
title: t('favorites.title'),
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="settings"
|
||||
options={{
|
||||
title: t('settings.title'),
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
61
client/app/(app)/favorites.tsx
Normal file
61
client/app/(app)/favorites.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { FlatList, StyleSheet, Text, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { MOCK_CONTENT } from '@/src/constants/mockContent';
|
||||
import { getFavorites } from '@/src/storage/appStorage';
|
||||
|
||||
export default function FavoritesScreen() {
|
||||
const { t } = useTranslation();
|
||||
const [ids, setIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const list = await getFavorites();
|
||||
if (!cancelled) setIds(list);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const map = new Map(MOCK_CONTENT.map((c) => [c.id, c]));
|
||||
return ids.map((id) => map.get(id)).filter(Boolean) as { id: string; text: string }[];
|
||||
}, [ids]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{items.length === 0 ? (
|
||||
<Text style={styles.empty}>{t('favorites.empty')}</Text>
|
||||
) : (
|
||||
<FlatList
|
||||
data={items}
|
||||
keyExtractor={(it) => it.id}
|
||||
contentContainerStyle={styles.list}
|
||||
renderItem={({ item }) => (
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.text}>{item.text}</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, padding: 16 },
|
||||
empty: { color: '#6B7280', fontSize: 16, textAlign: 'center', marginTop: 40 },
|
||||
list: { gap: 12, paddingBottom: 24 },
|
||||
row: {
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
backgroundColor: '#F9FAFB',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
text: { color: '#111827', fontSize: 16, lineHeight: 22 },
|
||||
});
|
||||
|
||||
76
client/app/(app)/home.tsx
Normal file
76
client/app/(app)/home.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { MOCK_CONTENT } from '@/src/constants/mockContent';
|
||||
import { addFavorite, setReaction } from '@/src/storage/appStorage';
|
||||
|
||||
export default function HomeScreen() {
|
||||
const { t } = useTranslation();
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
const item = useMemo(() => MOCK_CONTENT[index % MOCK_CONTENT.length], [index]);
|
||||
|
||||
async function onLike() {
|
||||
await setReaction(item.id, 'like');
|
||||
await addFavorite(item.id);
|
||||
setIndex((i) => i + 1);
|
||||
}
|
||||
|
||||
async function onDislike() {
|
||||
await setReaction(item.id, 'dislike');
|
||||
setIndex((i) => i + 1);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.text}>{item.text}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.actions}>
|
||||
<Pressable style={[styles.button, styles.dislike]} onPress={onDislike}>
|
||||
<Text style={styles.buttonText}>{t('home.dislike')}</Text>
|
||||
</Pressable>
|
||||
<Pressable style={[styles.button, styles.like]} onPress={onLike}>
|
||||
<Text style={styles.buttonText}>{t('home.like')}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
justifyContent: 'center',
|
||||
gap: 16,
|
||||
},
|
||||
card: {
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
backgroundColor: '#F6F7FB',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
text: {
|
||||
fontSize: 20,
|
||||
lineHeight: 28,
|
||||
color: '#111827',
|
||||
},
|
||||
actions: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
button: {
|
||||
flex: 1,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
},
|
||||
like: { backgroundColor: '#16A34A' },
|
||||
dislike: { backgroundColor: '#EF4444' },
|
||||
buttonText: { color: 'white', fontSize: 16, fontWeight: '600' },
|
||||
});
|
||||
|
||||
50
client/app/(app)/settings.tsx
Normal file
50
client/app/(app)/settings.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import Constants from 'expo-constants';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const version =
|
||||
Constants.expoConfig?.version ??
|
||||
// 兜底:部分环境下 expoConfig 可能为空
|
||||
'unknown';
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.label}>{t('settings.version')}</Text>
|
||||
<Text style={styles.value}>{version}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>{t('settings.widgetTitle')}</Text>
|
||||
<Text style={styles.cardText}>{t('settings.widgetDesc')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, padding: 16, gap: 16 },
|
||||
section: {
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: '#E5E7EB',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
label: { color: '#374151', fontSize: 16 },
|
||||
value: { color: '#111827', fontSize: 16, fontWeight: '600' },
|
||||
card: {
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
backgroundColor: '#111827',
|
||||
},
|
||||
cardTitle: { color: 'white', fontSize: 16, fontWeight: '700', marginBottom: 8 },
|
||||
cardText: { color: '#E5E7EB', fontSize: 14, lineHeight: 20 },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user