Inspired by a dashboard design the user shared. - App: new palette and fonts, borderless rounded panels with soft shadows, a pink-to-blue gradient card for your own song (text and controls switch to white on it), flat cards for friends, a 'Hi Chris, welcome back' header, an outline bell, and a pink pill on the active tab. - Website: the same look, plus a wide-screen layout with a sidebar, the page in a rounded panel, and a right column with your profile, recent activity and recent plays. Phones keep the top bar and bottom tabs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y2iUsKEWDuRGjK5FPmcUP
49 lines
2.2 KiB
TypeScript
49 lines
2.2 KiB
TypeScript
import { Image } from 'expo-image';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
import { useEffect } from 'react';
|
|
import { StyleSheet, Text, View } from 'react-native';
|
|
import Animated, { Easing, cancelAnimation, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated';
|
|
import type { UserSummaryDTO } from '@musicshare/shared';
|
|
import { colors, fonts } from '@/lib/theme';
|
|
|
|
/** `live` wraps the avatar in a spinning gradient ring (someone is listening right now). */
|
|
export function Avatar({ user, size = 40, live }: { user: UserSummaryDTO; size?: number; live?: boolean }) {
|
|
const rotation = useSharedValue(0);
|
|
useEffect(() => {
|
|
if (live) rotation.set(withRepeat(withTiming(360, { duration: 3500, easing: Easing.linear }), -1, false));
|
|
else cancelAnimation(rotation);
|
|
return () => cancelAnimation(rotation);
|
|
}, [live, rotation]);
|
|
const spin = useAnimatedStyle(() => ({ transform: [{ rotate: `${rotation.value}deg` }] }));
|
|
|
|
const shape = { width: size, height: size, borderRadius: size / 2 };
|
|
const face = user.avatarUrl ? (
|
|
<Image source={user.avatarUrl} style={shape} contentFit="cover" />
|
|
) : (
|
|
<LinearGradient colors={[colors.accent, '#8b5cf6']} start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }} style={[styles.empty, shape]}>
|
|
<Text style={[styles.letter, { fontSize: size * 0.45 }]}>{(user.displayName ?? user.username).slice(0, 1).toUpperCase()}</Text>
|
|
</LinearGradient>
|
|
);
|
|
if (!live) return face;
|
|
|
|
const ring = size + 6;
|
|
return (
|
|
<View style={{ width: ring, height: ring, alignItems: 'center', justifyContent: 'center' }}>
|
|
<Animated.View style={[StyleSheet.absoluteFill, spin]}>
|
|
<LinearGradient
|
|
colors={[colors.accent, colors.accent2, colors.live, colors.accent]}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
style={{ width: ring, height: ring, borderRadius: ring / 2 }}
|
|
/>
|
|
</Animated.View>
|
|
<View style={{ borderRadius: (size + 2) / 2, borderWidth: 2, borderColor: colors.bg }}>{face}</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
empty: { alignItems: 'center', justifyContent: 'center' },
|
|
letter: { color: '#fff', fontFamily: fonts.display },
|
|
});
|