You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

245 lines
7.7 KiB

import { StatusBar } from 'expo-status-bar';
import { FlatList, Keyboard, Modal, Pressable, ScrollView, StyleSheet, View } from 'react-native';
import { SafeAreaProvider, SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider';
import '@/global.css';
import { useWordbook, WordbookProvider } from './lib/wordbook/context';
import { Text } from './components/ui/text';
import { useMemo, useState } from 'react';
import { jaPack, languages } from './lib/providers/dictionaries';
import { Input, InputField } from './components/ui/input';
import { HStack } from './components/ui/hstack';
import { Button, ButtonIcon, ButtonText } from './components/ui/button';
import DictWebView from './components/DictWebView';
import AdBannerPlaceholder from './components/AdBannerPlaceholder';
import { Toast, ToastDescription, ToastTitle, useToast } from './components/ui/toast';
import mobileAds from 'react-native-google-mobile-ads';
import { Icon } from './components/ui/icon';
import { Bookmark, BookMarked, BookOpen, Search } from 'lucide-react-native';
function MainScreen() {
const [q, setQ] = useState('');
const [query, setQuery] = useState('');
const [activeId, setActiveId] = useState<string>('');
const [wordbookOpen, setWordbookOpen] = useState(false);
const insets = useSafeAreaInsets();
const pack = jaPack;
const providers = pack.providers;
const canSearch = q.trim().length > 0;
const hasQuery = query.trim().length > 0;
const toast = useToast();
const { add, items, remove, clear, loading } = useWordbook();
const urls = useMemo(
() =>
providers.reduce<Record<string, string>>((acc, p) => {
acc[p.id] = hasQuery ? p.buildUrl(query.trim()) : '';
return acc;
}, {}),
[providers, query, hasQuery],
);
const onSearch = () => {
if (!canSearch) return;
Keyboard.dismiss();
setQuery(q.trim());
setActiveId(providers[0]?.id ?? '');
};
const onSave = () => {
if (!canSearch) return;
Keyboard.dismiss();
add(q.trim(), pack.code);
toast.show({
id: 'onSave',
placement: 'top',
duration: 2000,
render: () => (
<View style={{ marginTop: insets.top + 12, alignItems: 'center', width: '100%' }}>
<Toast
className="rounded-full opacity-80 mx-5 py-2 flex-row items-center gap-2 shadow-lg max-w-[90%]"
variant="solid"
>
<ToastTitle></ToastTitle>
<ToastDescription> </ToastDescription>
</Toast>
</View>
),
});
};
const onPickWord = (word: string) => {
const t = word.trim();
setWordbookOpen(false);
setQ(t);
setQuery(t);
setActiveId(providers[0]?.id ?? '');
};
return (
<View className="flex-1">
{/* 상단 검색창, 버튼 */}
<View className="p-4 gap-3">
{/* 검색창 */}
<HStack space="md" className="flex-row">
<Button
className="p-1"
onPress={() => setWordbookOpen(true)}
variant="outline"
action="secondary"
accessibilityLabel="단어장 보기"
>
<Icon as={BookMarked} />
</Button>
<Input className="flex-1">
<InputField
placeholder="검색어를 입력하세요"
value={q}
onChangeText={setQ}
returnKeyType="search"
submitBehavior="blurAndSubmit"
onSubmitEditing={onSearch}
/>
</Input>
<Button
className="p-1"
onPress={onSave}
variant="outline"
action="secondary"
isDisabled={!canSearch}
accessibilityLabel="단어장 저장"
>
<Icon as={Bookmark} />
</Button>
<Button onPress={onSearch} isDisabled={!canSearch}>
<ButtonText></ButtonText>
</Button>
</HStack>
</View>
{/* 커스텀 탭 헤더 (가로스크롤) */}
<View className="px-4 pb-2">
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerClassName="gap-1"
>
{providers.map((p) => {
const selected = activeId ? activeId === p.id : false;
return (
<Button
className="flex-1"
key={p.id}
variant={selected ? 'solid' : 'outline'}
onPress={() => setActiveId(p.id)}
isDisabled={!canSearch}
>
<ButtonText>{p.label}</ButtonText>
</Button>
);
})}
</ScrollView>
</View>
{/* 패널: WebView 모두 마운트 후 토글 */}
<View className="flex-1">
{!hasQuery ? (
<View className="flex-1 items-center justify-center">
<Text> [] </Text>
</View>
) : activeId ? (
<DictWebView url={urls[activeId]} />
) : (
<View className="flex-1 items-center justify-center">
<Text> </Text>
</View>
)}
</View>
<AdBannerPlaceholder />
<Modal
visible={wordbookOpen}
animationType="slide"
onRequestClose={() => setWordbookOpen(false)}
>
<View className="flex-1 p-4 gap-3">
<View className="flex-row justify-between items-center">
<Text className="text-lg font-semibold"></Text>
<HStack space="md" className="flex-row">
<Button action="negative" onPress={clear}>
<ButtonText></ButtonText>
</Button>
<Button onPress={() => setWordbookOpen(false)}>
<ButtonText></ButtonText>
</Button>
</HStack>
</View>
{loading ? (
<View className="flex-1 items-center justify-center">
<Text> ...</Text>
</View>
) : (
<FlatList
className="flex-1"
data={items}
keyExtractor={(x) => x.id}
renderItem={({ item }) => (
<View className="flex-row justify-between py-1">
<Pressable
onPress={() => onPickWord(item.text)}
style={{ flex: 1 }}
className="justify-center items-center"
>
<Text className="text-lg">{item.text}</Text>
</Pressable>
<Button action="secondary" onPress={() => remove(item.id)}>
<ButtonText></ButtonText>
</Button>
</View>
)}
/>
)}
</View>
</Modal>
</View>
);
}
export default function App() {
// mobileAds()
// .initialize()
// .then(() => {
// // ready
// });
return (
<GluestackUIProvider mode="light">
<SafeAreaProvider>
<SafeAreaView style={{ flex: 1 }}>
<StatusBar style="auto" />
<WordbookProvider>
<MainScreen />
</WordbookProvider>
</SafeAreaView>
</SafeAreaProvider>
</GluestackUIProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});