Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions app/(tabs)/(folder)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
View,
type KeyboardEvent,
} from 'react-native';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { AddFolderButton } from '@/components/ui/add-folder-button';
Expand All @@ -34,6 +35,11 @@ type MenuState = {
folderId?: number;
};

const CONTENT_HORIZONTAL_PADDING = 24;
const CANVAS_PADDING = 16;
const FOLDER_GRID_GAP = 12;
const DEFAULT_FOLDER_CARD_WIDTH = 144;
const MIN_TWO_COLUMN_CARD_WIDTH = 120;
const RENAME_MODAL_BOTTOM_GAP = 16;
const RENAME_KEYBOARD_TOP_GAP = 8;

Expand All @@ -59,6 +65,7 @@ const syncKeyboardLayoutAnimation = (event: KeyboardEvent) => {
export default function FolderScreen() {
const insets = useSafeAreaInsets();
const router = useRouter();
const tabBarHeight = useBottomTabBarHeight();
const { folderCreated: folderCreatedParam } = useLocalSearchParams<{
folderCreated?: string | string[];
}>();
Expand Down Expand Up @@ -86,6 +93,7 @@ export default function FolderScreen() {
const [createToastVisible, setCreateToastVisible] = useState(false);
const [deleteToastVisible, setDeleteToastVisible] = useState(false);
const [renameToastVisible, setRenameToastVisible] = useState(false);
const [gridWidth, setGridWidth] = useState(0);
const lastCreatedToastRef = useRef<string | undefined>(undefined);
const isMutatingRef = useRef(false);
const renameInputRef = useRef<TextInput>(null);
Expand All @@ -96,6 +104,27 @@ export default function FolderScreen() {
() => rawFolders.map((folder) => ({ ...folder })),
[rawFolders],
);
const folderCardWidth = useMemo(() => {
const availableWidth = gridWidth;

if (availableWidth <= 0) {
return DEFAULT_FOLDER_CARD_WIDTH;
}

const defaultTwoColumnWidth = DEFAULT_FOLDER_CARD_WIDTH * 2 + FOLDER_GRID_GAP;

if (availableWidth >= defaultTwoColumnWidth) {
return DEFAULT_FOLDER_CARD_WIDTH;
}

const compactTwoColumnWidth = Math.floor((availableWidth - FOLDER_GRID_GAP) / 2);

if (compactTwoColumnWidth >= MIN_TWO_COLUMN_CARD_WIDTH) {
return compactTwoColumnWidth;
}

return availableWidth;
}, [gridWidth]);

const handleMorePress = (folderId: number, anchor: AnchorPosition) => {
setMenuState({ visible: true, anchor, folderId });
Expand Down Expand Up @@ -249,7 +278,7 @@ export default function FolderScreen() {
<SafeAreaView style={styles.safeArea} edges={['top']}>
<ScrollView
style={styles.scroll}
contentContainerStyle={styles.content}
contentContainerStyle={[styles.content, { paddingBottom: tabBarHeight + 24 }]}
showsVerticalScrollIndicator={false}
>
{/* 상단 헤더 */}
Expand All @@ -276,12 +305,16 @@ export default function FolderScreen() {
<ActivityIndicator color={Colors.brand.primary} />
</View>
) : folders.length > 0 ? (
<View style={styles.grid}>
<View
style={styles.grid}
onLayout={(event) => setGridWidth(event.nativeEvent.layout.width)}
>
{folders.map((folder) => (
<FolderCard
key={folder.id}
folderName={folder.name}
urlCount={folder.linkCount}
width={folderCardWidth}
onPress={() => router.push({ pathname: '/(tabs)/(folder)/[id]' as any, params: { id: folder.id } })}
onMorePress={(anchor) => handleMorePress(folder.id, anchor)}
/>
Expand Down Expand Up @@ -403,7 +436,7 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
paddingHorizontal: 24,
paddingHorizontal: CONTENT_HORIZONTAL_PADDING,
paddingBottom: 32,
gap: 20,
},
Expand All @@ -426,13 +459,14 @@ const styles = StyleSheet.create({
borderRadius: 20,
borderWidth: 1,
borderColor: Colors.brand.line,
padding: 16,
padding: CANVAS_PADDING,
gap: 16,
},
grid: {
width: '100%',
flexDirection: 'row',
flexWrap: 'wrap',
gap: 12,
gap: FOLDER_GRID_GAP,
},
errorBox: {
borderRadius: 12,
Expand Down
55 changes: 46 additions & 9 deletions app/(tabs)/(home)/add-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Text,
TextInput,
TouchableOpacity,
useWindowDimensions,
View,
} from 'react-native';
import { Stack, router, useLocalSearchParams } from 'expo-router';
Expand All @@ -16,6 +17,9 @@ import { Colors, Typography } from '@/constants/theme';
import { useGuardedPress } from '@/utils/press-guard';
import { normalizeHttpUrlInput } from '@/utils/shared-url';

const COMPACT_WIDTH = 380;
const SHORT_SCREEN_HEIGHT = 760;

function getSharedUrlParam(value: string | string[] | undefined): string {
if (typeof value !== 'string') {
return '';
Expand All @@ -26,9 +30,11 @@ function getSharedUrlParam(value: string | string[] | undefined): string {

export default function AddLinkScreen() {
const { sharedUrl } = useLocalSearchParams<{ sharedUrl?: string }>();
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const initialSharedUrl = getSharedUrlParam(sharedUrl);
const [url, setUrl] = useState(initialSharedUrl);
const [error, setError] = useState('');
const isCompact = windowWidth < COMPACT_WIDTH || windowHeight <= SHORT_SCREEN_HEIGHT;
const [isNavigating, setIsNavigating] = useState(false);
const isNavigatingRef = useRef(false);

Expand Down Expand Up @@ -110,19 +116,21 @@ export default function AddLinkScreen() {
style={styles.flex}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<View style={styles.container}>
<View style={[styles.container, isCompact && styles.containerCompact]}>
{/* 타이틀 */}
<View style={styles.titleRow}>
<Text style={styles.titleGreen}>링크를</Text>
<Text style={styles.titleDark}> 입력해주세요</Text>
<View style={[styles.titleRow, isCompact && styles.titleRowCompact]}>
<Text style={[styles.titleGreen, isCompact && styles.titleCompact]}>링크를</Text>
<Text style={[styles.titleDark, isCompact && styles.titleCompact]}> 입력해주세요</Text>
</View>

{/* 부제목 */}
<Text style={styles.subtitle}>보안검사 후 저장할 수 있습니다.</Text>
<Text style={[styles.subtitle, isCompact && styles.subtitleCompact]}>
보안검사 후 저장할 수 있습니다.
</Text>

{/* URL 입력 + 검사 버튼 */}
<View style={styles.inputRow}>
<View style={[styles.inputWrapper, hasError && styles.inputError]}>
<View style={[styles.inputRow, isCompact && styles.inputRowCompact]}>
<View style={[styles.inputWrapper, isCompact && styles.inputWrapperCompact, hasError && styles.inputError]}>
<TextInput
style={styles.input}
placeholder="https://example.com"
Expand All @@ -149,10 +157,10 @@ export default function AddLinkScreen() {
</View>

{/* 에러 메시지 */}
{hasError && <Text style={styles.errorText}>{error}</Text>}
{hasError && <Text style={[styles.errorText, isCompact && styles.errorTextCompact]}>{error}</Text>}

{/* 안내 박스 */}
<View style={styles.infoBox}>
<View style={[styles.infoBox, isCompact && styles.infoBoxCompact]}>
<Text style={styles.infoLabel}>안내</Text>
<Text style={styles.infoBody}>
검사 후 안전한 사이트이라면, 저장하실 수 있습니다.
Expand All @@ -178,13 +186,20 @@ const styles = StyleSheet.create({
paddingHorizontal: 24,
paddingTop: 16,
},
containerCompact: {
paddingHorizontal: 20,
paddingTop: 8,
},

// 타이틀
titleRow: {
flexDirection: 'row',
flexWrap: 'wrap',
marginBottom: 12,
},
titleRowCompact: {
marginBottom: 8,
},
titleGreen: {
...Typography.display,
color: Colors.brand.primaryDeep,
Expand All @@ -193,13 +208,20 @@ const styles = StyleSheet.create({
...Typography.display,
color: Colors.brand.text,
},
titleCompact: {
...Typography.pageTitle,
},

// 부제목
subtitle: {
...Typography.body,
color: Colors.brand.textSecondary,
marginBottom: 32,
},
subtitleCompact: {
...Typography.summary,
marginBottom: 24,
},

// 입력 행
inputRow: {
Expand All @@ -208,6 +230,9 @@ const styles = StyleSheet.create({
gap: 8,
marginBottom: 8,
},
inputRowCompact: {
gap: 6,
},
inputWrapper: {
flex: 1,
flexDirection: 'row',
Expand All @@ -219,6 +244,10 @@ const styles = StyleSheet.create({
backgroundColor: Colors.brand.surface,
paddingHorizontal: 16,
},
inputWrapperCompact: {
height: 56,
paddingHorizontal: 14,
},
inputError: {
borderColor: Colors.brand.textWarning,
},
Expand Down Expand Up @@ -247,6 +276,10 @@ const styles = StyleSheet.create({
color: Colors.brand.textWarning,
marginBottom: 16,
},
errorTextCompact: {
...Typography.summary,
marginBottom: 12,
},

// 안내 박스
infoBox: {
Expand All @@ -256,6 +289,10 @@ const styles = StyleSheet.create({
marginTop: 16,
gap: 6,
},
infoBoxCompact: {
padding: 14,
marginTop: 12,
},
infoLabel: {
...Typography.caption,
color: Colors.brand.primaryDeep,
Expand Down
Loading
Loading