384 lines
13 KiB
JavaScript
384 lines
13 KiB
JavaScript
import React, { useState, useCallback, useRef } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
Image,
|
|
TouchableOpacity,
|
|
Animated,
|
|
Dimensions,
|
|
Alert,
|
|
Share,
|
|
FlatList,
|
|
} from 'react-native';
|
|
import { useFocusEffect } from '@react-navigation/native';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
import { BlurView } from 'expo-blur';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { getConcertById, deleteConcert } from '../utils/storage';
|
|
import { getGenreTheme } from '../constants/genres';
|
|
|
|
const { width, height } = Dimensions.get('window');
|
|
|
|
export default function ConcertDetailScreen({ navigation, route }) {
|
|
const { id } = route.params;
|
|
const [concert, setConcert] = useState(null);
|
|
const scrollY = useRef(new Animated.Value(0)).current;
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
loadConcert();
|
|
}, [id])
|
|
);
|
|
|
|
const loadConcert = async () => {
|
|
const data = await getConcertById(id);
|
|
setConcert(data);
|
|
};
|
|
|
|
if (!concert) return <View style={{ flex: 1, backgroundColor: '#080808' }} />;
|
|
|
|
const theme = getGenreTheme(concert.genre);
|
|
const c = theme.colors;
|
|
|
|
const formattedDate = concert.date
|
|
? new Date(concert.date).toLocaleDateString('de-DE', {
|
|
weekday: 'long',
|
|
day: '2-digit',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
})
|
|
: null;
|
|
|
|
const headerScale = scrollY.interpolate({
|
|
inputRange: [-100, 0],
|
|
outputRange: [1.2, 1],
|
|
extrapolateRight: 'clamp',
|
|
});
|
|
|
|
const headerOpacity = scrollY.interpolate({
|
|
inputRange: [0, 200],
|
|
outputRange: [1, 0],
|
|
extrapolate: 'clamp',
|
|
});
|
|
|
|
const navOpacity = scrollY.interpolate({
|
|
inputRange: [150, 250],
|
|
outputRange: [0, 1],
|
|
extrapolate: 'clamp',
|
|
});
|
|
|
|
const handleDelete = () => {
|
|
Alert.alert(
|
|
'Konzert löschen',
|
|
'Möchtest du diesen Eintrag wirklich löschen?',
|
|
[
|
|
{ text: 'Abbrechen', style: 'cancel' },
|
|
{
|
|
text: 'Löschen',
|
|
style: 'destructive',
|
|
onPress: async () => {
|
|
await deleteConcert(id);
|
|
navigation.goBack();
|
|
},
|
|
},
|
|
]
|
|
);
|
|
};
|
|
|
|
const handleShare = async () => {
|
|
try {
|
|
await Share.share({
|
|
message: `🎵 ${concert.title || 'Konzert'}\n${concert.artist ? `👤 ${concert.artist}` : ''}\n${concert.venue ? `📍 ${concert.venue}` : ''}\n${formattedDate || ''}\n\nGeteilt via SETLIST App`,
|
|
});
|
|
} catch (e) {}
|
|
};
|
|
|
|
const renderStars = (rating) =>
|
|
[1, 2, 3, 4, 5].map((s) => (
|
|
<Text key={s} style={{ fontSize: 20, color: s <= rating ? c.primary : '#333' }}>
|
|
★
|
|
</Text>
|
|
));
|
|
|
|
const renderGalleryItem = ({ item }) => (
|
|
<TouchableOpacity
|
|
onPress={() => navigation.navigate('FullscreenImage', { uri: item, images: concert.gallery })}
|
|
activeOpacity={0.9}
|
|
>
|
|
<Image source={{ uri: item }} style={[styles.galleryThumb, { borderColor: c.primary + '40' }]} />
|
|
</TouchableOpacity>
|
|
);
|
|
|
|
return (
|
|
<View style={[styles.container, { backgroundColor: c.background }]}>
|
|
{/* Floating Nav on scroll */}
|
|
<Animated.View style={[styles.floatingNav, { opacity: navOpacity }]}>
|
|
<BlurView intensity={80} tint="dark" style={StyleSheet.absoluteFill} />
|
|
<Text style={[styles.floatingNavTitle, { color: c.text }]} numberOfLines={1}>
|
|
{concert.title}
|
|
</Text>
|
|
</Animated.View>
|
|
|
|
{/* Back / Actions */}
|
|
<View style={styles.navBar}>
|
|
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.navBtn} activeOpacity={0.7}>
|
|
<BlurView intensity={60} tint="dark" style={StyleSheet.absoluteFill} />
|
|
<Ionicons name="chevron-back" size={22} color="#FFF" />
|
|
</TouchableOpacity>
|
|
<View style={styles.navRight}>
|
|
<TouchableOpacity onPress={handleShare} style={styles.navBtn} activeOpacity={0.7}>
|
|
<BlurView intensity={60} tint="dark" style={StyleSheet.absoluteFill} />
|
|
<Ionicons name="share-outline" size={20} color="#FFF" />
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
onPress={() => navigation.navigate('EditConcert', { id: concert.id })}
|
|
style={styles.navBtn}
|
|
activeOpacity={0.7}
|
|
>
|
|
<BlurView intensity={60} tint="dark" style={StyleSheet.absoluteFill} />
|
|
<Ionicons name="pencil-outline" size={20} color="#FFF" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
<Animated.ScrollView
|
|
showsVerticalScrollIndicator={false}
|
|
onScroll={Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], {
|
|
useNativeDriver: true,
|
|
})}
|
|
scrollEventThrottle={16}
|
|
>
|
|
{/* Hero Image */}
|
|
<Animated.View style={[styles.heroContainer, { transform: [{ scale: headerScale }] }]}>
|
|
{concert.ticketImage ? (
|
|
<TouchableOpacity
|
|
onPress={() =>
|
|
navigation.navigate('FullscreenImage', {
|
|
uri: concert.ticketImage,
|
|
images: [concert.ticketImage],
|
|
})
|
|
}
|
|
activeOpacity={0.95}
|
|
>
|
|
<Image source={{ uri: concert.ticketImage }} style={styles.heroImage} resizeMode="cover" />
|
|
</TouchableOpacity>
|
|
) : (
|
|
<LinearGradient
|
|
colors={c.gradient}
|
|
style={styles.heroPlaceholder}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
>
|
|
<Text style={styles.heroEmoji}>{theme.icon}</Text>
|
|
</LinearGradient>
|
|
)}
|
|
<Animated.View style={[styles.heroOverlay, { opacity: headerOpacity }]}>
|
|
<LinearGradient
|
|
colors={['transparent', c.background]}
|
|
style={StyleSheet.absoluteFill}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 0, y: 1 }}
|
|
/>
|
|
</Animated.View>
|
|
</Animated.View>
|
|
|
|
{/* Content */}
|
|
<View style={[styles.content, { backgroundColor: c.background }]}>
|
|
{/* Genre + Date tag */}
|
|
<View style={styles.tagsRow}>
|
|
<View style={[styles.tag, { backgroundColor: c.primary + '30', borderColor: c.primary + '60' }]}>
|
|
<Text style={[styles.tagText, { color: c.primary }]}>
|
|
{theme.icon} {theme.label}
|
|
</Text>
|
|
</View>
|
|
{formattedDate && (
|
|
<View style={[styles.tag, { backgroundColor: '#FFFFFF10', borderColor: '#FFFFFF20' }]}>
|
|
<Ionicons name="calendar-outline" size={11} color={c.textMuted} />
|
|
<Text style={[styles.tagText, { color: c.textMuted }]}>{formattedDate}</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* Title */}
|
|
<Text style={[styles.title, { color: c.text }]}>{concert.title || 'Unbenannt'}</Text>
|
|
|
|
{/* Artist */}
|
|
{concert.artist && (
|
|
<Text style={[styles.artist, { color: c.primary }]}>{concert.artist}</Text>
|
|
)}
|
|
|
|
{/* Venue */}
|
|
{concert.venue && (
|
|
<View style={styles.venueRow}>
|
|
<Ionicons name="location" size={16} color={c.textMuted} />
|
|
<Text style={[styles.venue, { color: c.textMuted }]}>{concert.venue}</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Rating */}
|
|
{concert.rating > 0 && (
|
|
<View style={styles.ratingRow}>
|
|
{renderStars(concert.rating)}
|
|
<Text style={[styles.ratingLabel, { color: c.textMuted }]}>
|
|
{['', 'Enttäuschend', 'Okay', 'Gut', 'Sehr gut', 'Unvergesslich'][concert.rating]}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Divider */}
|
|
<LinearGradient
|
|
colors={[c.primary + '60', 'transparent']}
|
|
style={styles.divider}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 0 }}
|
|
/>
|
|
|
|
{/* Notes */}
|
|
{concert.notes && concert.notes.trim() !== '' && (
|
|
<View style={styles.section}>
|
|
<Text style={[styles.sectionTitle, { color: c.textMuted }]}>Erinnerungen</Text>
|
|
<Text style={[styles.notes, { color: c.text }]}>{concert.notes}</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Setlist */}
|
|
{concert.setlist && concert.setlist.length > 0 && (
|
|
<View style={styles.section}>
|
|
<Text style={[styles.sectionTitle, { color: c.textMuted }]}>Setlist</Text>
|
|
{concert.setlist.map((song, i) => (
|
|
<View key={i} style={styles.setlistItem}>
|
|
<Text style={[styles.setlistNum, { color: c.primary }]}>{String(i + 1).padStart(2, '0')}</Text>
|
|
<Text style={[styles.setlistSong, { color: c.text }]}>{song}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
|
|
{/* Gallery */}
|
|
{concert.gallery && concert.gallery.length > 0 && (
|
|
<View style={styles.section}>
|
|
<View style={styles.sectionHeader}>
|
|
<Text style={[styles.sectionTitle, { color: c.textMuted }]}>Galerie</Text>
|
|
<Text style={[styles.sectionCount, { color: c.primary }]}>
|
|
{concert.gallery.length} Fotos
|
|
</Text>
|
|
</View>
|
|
<FlatList
|
|
data={concert.gallery}
|
|
keyExtractor={(item, i) => i.toString()}
|
|
renderItem={renderGalleryItem}
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
contentContainerStyle={styles.galleryList}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{/* Delete */}
|
|
<TouchableOpacity onPress={handleDelete} style={styles.deleteBtn} activeOpacity={0.7}>
|
|
<Ionicons name="trash-outline" size={16} color="#FF4444" />
|
|
<Text style={styles.deleteBtnText}>Eintrag löschen</Text>
|
|
</TouchableOpacity>
|
|
|
|
<View style={{ height: 60 }} />
|
|
</View>
|
|
</Animated.ScrollView>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1 },
|
|
floatingNav: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
height: 90,
|
|
zIndex: 20,
|
|
alignItems: 'center',
|
|
justifyContent: 'flex-end',
|
|
paddingBottom: 12,
|
|
overflow: 'hidden',
|
|
},
|
|
floatingNavTitle: { fontSize: 16, fontWeight: '700', letterSpacing: 0.5 },
|
|
navBar: {
|
|
position: 'absolute',
|
|
top: 52,
|
|
left: 0,
|
|
right: 0,
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: 20,
|
|
zIndex: 30,
|
|
},
|
|
navBtn: {
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 20,
|
|
overflow: 'hidden',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: 'rgba(0,0,0,0.4)',
|
|
},
|
|
navRight: { flexDirection: 'row', gap: 10 },
|
|
heroContainer: { height: 380, overflow: 'hidden' },
|
|
heroImage: { width: '100%', height: '100%' },
|
|
heroPlaceholder: {
|
|
width: '100%',
|
|
height: '100%',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
heroEmoji: { fontSize: 100 },
|
|
heroOverlay: { position: 'absolute', bottom: 0, left: 0, right: 0, height: 120 },
|
|
content: { flex: 1, paddingHorizontal: 20, paddingTop: 16 },
|
|
tagsRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 12 },
|
|
tag: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 5,
|
|
borderRadius: 8,
|
|
borderWidth: 1,
|
|
gap: 4,
|
|
},
|
|
tagText: { fontSize: 12, fontWeight: '600' },
|
|
title: { fontSize: 32, fontWeight: '900', letterSpacing: -0.5, marginBottom: 4, lineHeight: 38 },
|
|
artist: { fontSize: 18, fontWeight: '700', letterSpacing: 0.3, marginBottom: 8 },
|
|
venueRow: { flexDirection: 'row', alignItems: 'center', gap: 4, marginBottom: 16 },
|
|
venue: { fontSize: 14 },
|
|
ratingRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 16 },
|
|
ratingLabel: { fontSize: 13, marginLeft: 4 },
|
|
divider: { height: 1, marginBottom: 24 },
|
|
section: { marginBottom: 28 },
|
|
sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 },
|
|
sectionTitle: { fontSize: 11, fontWeight: '700', letterSpacing: 2, textTransform: 'uppercase', marginBottom: 12 },
|
|
sectionCount: { fontSize: 12, fontWeight: '600' },
|
|
notes: { fontSize: 16, lineHeight: 26, fontWeight: '400' },
|
|
setlistItem: { flexDirection: 'row', alignItems: 'center', gap: 12, marginBottom: 10 },
|
|
setlistNum: { fontSize: 12, fontWeight: '800', width: 24 },
|
|
setlistSong: { fontSize: 16, flex: 1 },
|
|
galleryList: { gap: 10 },
|
|
galleryThumb: {
|
|
width: 140,
|
|
height: 140,
|
|
borderRadius: 12,
|
|
borderWidth: 1,
|
|
},
|
|
deleteBtn: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 8,
|
|
paddingVertical: 14,
|
|
borderRadius: 12,
|
|
borderWidth: 1,
|
|
borderColor: '#FF444430',
|
|
backgroundColor: '#FF44440A',
|
|
marginTop: 8,
|
|
},
|
|
deleteBtnText: { color: '#FF4444', fontSize: 14, fontWeight: '600' },
|
|
});
|