Setlist/src/screens/ConcertDetailScreen.js
2026-03-24 11:31:10 +01:00

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' },
});