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
135 changes: 2 additions & 133 deletions app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,136 +1,5 @@
import { Feather } from '@expo/vector-icons';
import { Tabs, router } from 'expo-router';
import { Alert, Platform, Pressable, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { colors } from '@/constants/colors';
import { getTabBarHeight } from '@/constants/layout';
import { useTravelSessionStore } from '@/store/travelSessionStore';

type TabIconName = keyof typeof Feather.glyphMap;

const webGlassTabBarStyle = {
backdropFilter: 'blur(22px) saturate(150%)',
WebkitBackdropFilter: 'blur(22px) saturate(150%)',
};

function TabIcon({ name, color }: { name: TabIconName; color: string }) {
return <Feather color={color} name={name} size={22} />;
}

function CameraTabButton() {
const { session, startSession } = useTravelSessionStore();
const openCamera = () => router.push('/camera');
const handlePress = () => {
if (session.status === 'active') {
openCamera();
return;
}

Alert.alert('여행을 시작할까요?', '순간 저장은 현재 여행에 연결돼요.', [
{ style: 'cancel', text: '취소' },
{
onPress: () => {
startSession();
openCamera();
},
text: '시작하고 촬영',
},
]);
};

return (
<Pressable
accessibilityLabel="순간 저장 카메라 열기"
accessibilityRole="button"
className="-mt-6 h-[70px] w-[70px] items-center justify-center rounded-full border border-white/15 bg-soundlog-bg"
onPress={handlePress}
>
<View className="h-[58px] w-[58px] items-center justify-center rounded-full border-[3px] border-soundlog-lime bg-black/25">
<View className="h-[44px] w-[44px] rounded-full bg-soundlog-lime" />
</View>
</Pressable>
);
}
import { BottomNavigation } from '@/components/navigation/BottomNavigation';

export default function TabsLayout() {
const insets = useSafeAreaInsets();

return (
<Tabs
screenOptions={{
headerShown: false,
tabBarActiveTintColor: colors.accent.lime,
tabBarInactiveTintColor: colors.text.muted,
tabBarShowLabel: false,
tabBarStyle: {
position: 'absolute',
height: getTabBarHeight(insets.bottom),
paddingBottom: Math.max(insets.bottom, 12),
paddingTop: 10,
borderTopWidth: 1,
borderTopColor: 'rgba(255,255,255,0.24)',
backgroundColor: colors.surface.tab,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
boxShadow: '0 -18px 42px rgba(0,0,0,0.34)',
shadowColor: '#000',
shadowOffset: { height: -14, width: 0 },
shadowOpacity: 0.34,
shadowRadius: 24,
...(Platform.OS === 'web' ? webGlassTabBarStyle : {}),
},
}}
>
<Tabs.Screen
name="index"
options={{
tabBarIcon: ({ color }) => <TabIcon color={String(color)} name="home" />,
title: '홈',
}}
/>
<Tabs.Screen
name="recap"
options={{
tabBarIcon: ({ color }) => <TabIcon color={String(color)} name="map-pin" />,
title: 'Recap',
}}
/>
<Tabs.Screen
name="capture"
options={{
tabBarButton: () => <CameraTabButton />,
title: '기록',
}}
/>
<Tabs.Screen
name="library"
options={{
tabBarIcon: ({ color }) => <TabIcon color={String(color)} name="heart" />,
title: '보관함',
}}
/>
<Tabs.Screen
name="playlist/[id]"
options={{
href: null,
title: '플레이리스트',
}}
/>
<Tabs.Screen
name="recap-share/[id]"
options={{
href: null,
title: '리캡 공유',
}}
/>
<Tabs.Screen
name="my"
options={{
tabBarIcon: ({ color }) => <TabIcon color={String(color)} name="user" />,
title: '마이',
}}
/>
</Tabs>
);
return <BottomNavigation />;
}
114 changes: 57 additions & 57 deletions app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { router } from 'expo-router';
import { useCallback, useEffect, useState } from 'react';
import { ScrollView } from 'react-native';
import { useCallback, useEffect } from 'react';
import { ScrollView, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import {
Expand All @@ -13,6 +13,7 @@ import { MiniPlayer } from '@/components/MiniPlayer';
import { FeaturedPlaylistSection } from '@/components/home/FeaturedPlaylistSection';
import {
HomeHeader,
HomeNavigationBar,
HomeTopFilterBar,
isHomeTopFilter,
} from '@/components/home/HomeHeader';
Expand All @@ -21,7 +22,6 @@ import {
isMoodRecommendationFilter,
} from '@/components/home/MoodRecommendationSection';
import { MusicLogSection } from '@/components/home/MusicLogSection';
import { TravelModeSuggestionSheet } from '@/components/home/TravelModeSuggestionSheet';
import { TravelSessionCard } from '@/components/home/TravelSessionCard';
import { Screen } from '@/components/Screen';
import { getHomeContentBottomPadding } from '@/constants/layout';
Expand All @@ -40,8 +40,6 @@ import { createRecommendationEventContext } from '@/utils/recommendationEventCon

function HomeContent() {
const insets = useSafeAreaInsets();
const [dismissedSuggestionPlaceId, setDismissedSuggestionPlaceId] =
useState<string>();
const {
selectedMoodFilter,
selectedTopFilter,
Expand All @@ -62,13 +60,11 @@ function HomeContent() {
recommendationMode,
selectedMode,
session,
endSession,
resetSession,
setLocation,
setLocationStatus,
setMode,
setPlace,
setRecommendationMode,
startSession,
} = useTravelSessionStore();

const nearbyPlacesQuery = useNearbyPlacesQuery({
Expand Down Expand Up @@ -122,6 +118,17 @@ function HomeContent() {
}, [currentPlace?.id, nearbyPlacesQuery.data, setPlace]);

const handleSelectRecommendation = (item: MoodRecommendation) => {
if (item.playlistId) {
router.push(`/playlist/${item.playlistId}`);
addRecommendationEvent({
context: createRecommendationEventContext(),
playlistId: item.playlistId,
type: 'playlist_open',
value: item.playlistId,
});
return;
}

setTrack(item.track);
addRecommendationEvent({
context: createRecommendationEventContext(),
Expand Down Expand Up @@ -219,50 +226,49 @@ function HomeContent() {
profile.locationRecommendationEnabled,
]);

const shouldShowTravelModeSuggestion =
Boolean(currentPlace) &&
profile.locationRecommendationEnabled &&
recommendationMode === 'everyday' &&
dismissedSuggestionPlaceId !== currentPlace?.id;

return (
<Screen>
<ScrollView
className="flex-1"
contentContainerStyle={{
gap: 32,
gap: 20,
paddingBottom: getHomeContentBottomPadding(
insets.bottom,
Boolean(currentTrack),
),
paddingHorizontal: 20,
paddingTop: 24,
paddingTop: 16,
}}
showsVerticalScrollIndicator={false}
>
<HomeNavigationBar />

<HomeHeader
currentPlace={currentPlace}
isLocationLoading={locationStatus === 'loading'}
onSelectRecommendationMode={handleSelectRecommendationMode}
onSetCurrentLocation={handleSetCurrentLocation}
recommendationMode={recommendationMode}
/>

<TravelSessionCard
endedAt={session.endedAt}
onEndSession={endSession}
onOpenRecap={() => router.push('/recap')}
onSelectMode={setMode}
onStartSession={startSession}
selectedMode={selectedMode}
startedAt={session.startedAt}
status={session.status}
/>
{recommendationMode === 'travel' ? (
<View className="-mt-2">
<TravelSessionCard
currentPlace={currentPlace}
endedAt={session.endedAt}
onDismissEnded={resetSession}
onOpenRecap={() => router.push('/recap')}
onOpenTravel={() => router.push('/travel')}
selectedMode={selectedMode}
startedAt={session.startedAt}
status={session.status}
/>
</View>
) : null}

<HomeTopFilterBar
onSelectTopFilter={handleSelectTopFilter}
selectedTopFilter={selectedTopFilter}
/>
<View className={recommendationMode === 'travel' ? '' : '-mt-2'}>
<HomeTopFilterBar
onSelectTopFilter={handleSelectTopFilter}
selectedTopFilter={selectedTopFilter}
/>
</View>

<FeaturedPlaylistSection
data={featuredPlaylistsQuery.data}
Expand All @@ -271,33 +277,27 @@ function HomeContent() {
onRetry={() => void featuredPlaylistsQuery.refetch()}
/>

<MoodRecommendationSection
data={moodRecommendationsQuery.data}
isError={moodRecommendationsQuery.isError}
isLoading={moodRecommendationsQuery.isLoading}
onSelectMoodFilter={handleSelectMoodFilter}
onSelectRecommendation={handleSelectRecommendation}
selectedMoodFilter={selectedMoodFilter}
/>
<View className="mt-2">
<MoodRecommendationSection
data={moodRecommendationsQuery.data}
isError={moodRecommendationsQuery.isError}
isLoading={moodRecommendationsQuery.isLoading}
onSelectMoodFilter={handleSelectMoodFilter}
onSelectRecommendation={handleSelectRecommendation}
selectedMoodFilter={selectedMoodFilter}
/>
</View>

<MusicLogSection
data={musicLogs}
isError={recentMusicLogsQuery.isError}
isLoading={recentMusicLogsQuery.isLoading && momentLogs.length === 0}
onSelectLog={handleSelectMusicLog}
/>
<View className="mt-2">
<MusicLogSection
data={musicLogs}
isError={recentMusicLogsQuery.isError}
isLoading={recentMusicLogsQuery.isLoading && momentLogs.length === 0}
onSelectLog={handleSelectMusicLog}
/>
</View>
</ScrollView>
{currentTrack ? <MiniPlayer /> : null}
{shouldShowTravelModeSuggestion && currentPlace ? (
<TravelModeSuggestionSheet
onDismiss={() => setDismissedSuggestionPlaceId(currentPlace.id)}
onStartTravelMode={() => {
handleSelectRecommendationMode('travel');
setDismissedSuggestionPlaceId(currentPlace.id);
}}
place={currentPlace}
/>
) : null}
</Screen>
);
}
Expand Down
5 changes: 5 additions & 0 deletions app/(tabs)/travel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { TravelScreen } from '@/components/travel/TravelScreen';

export default function TravelTabScreen() {
return <TravelScreen />;
}
Binary file modified assets/soundlog-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions docs/frontend/SOUNDLOG_DESIGN_SYSTEM 2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Soundlog Design System

## Palette

The Soundlog palette is built from the reference colors:

| Token | Hex | Role |
| --- | --- | --- |
| `brand.pulseMagenta` | `#D718F1` | Personal taste, emotional highlights, everyday music energy |
| `brand.electricViolet` | `#4F2AEC` | Selected states, primary brand surfaces, active chips |
| `brand.signalPurple` | `#872BA8` | Supporting depth, recap and memory moments |
| `brand.limeWave` | `#B7E628` | High-priority action, travel mode, focus borders |
| `brand.deepIndigo` | `#3B11C4` | Deep anchors, player and navigation depth |

## Color Roles

Use role tokens from `src/constants/colors.ts` before adding screen-local colors.

- Backgrounds use near-black violet tones so the bright palette feels musical instead of flat.
- Surfaces use glassy purple cards and chips with white borders at low opacity.
- Active selections use electric violet with lime focus where the action should feel immediate.
- Primary CTAs and travel-state highlights use lime with `text.inverse`.
- Magenta is reserved for taste, mood, and expressive music moments.

## Tailwind Tokens

NativeWind tokens live under `soundlog` in `tailwind.config.js`.

- `bg-soundlog-bg`: app background
- `bg-soundlog-card`: standard cards
- `bg-soundlog-elevated`: emphasized panels
- `bg-soundlog-chip`: unselected chips and compact controls
- `bg-soundlog-selected`: selected chips and segmented controls
- `bg-soundlog-lime`: high-priority actions
- `border-soundlog-border`: standard purple border
- `border-soundlog-focus`: focus or active emphasis
- `text-soundlog-inverse`: text on lime or bright selected surfaces

## Component Rules

- Keep the camera capture action lime so it is recognizable across tabs.
- Keep tab active state lime and inactive state muted white.
- Chips should stay compact, rounded, and role-based: selected violet, unselected purple.
- Do not use the five source colors evenly on every screen. Each screen should have one dominant role and one accent.
- Avoid adding new hex values in components unless the value is data-driven, such as track artwork fallback colors.
Loading
Loading