- Web header is just the logo and Feed / Profile / Settings (current page highlighted). On phones those move to a bottom tab bar. Log out, Admin and Support moved into Settings. - The bottom of Settings (web and app) is now one Account list: email, password, blocked accounts, admin, support, privacy, terms, log out and delete, each a row that opens in place (web) or a screen (app). - Featured song shows what's featured now, with Change and Remove. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
@@ -2,18 +2,24 @@ import { Image } from 'expo-image';
|
||||
import { useState } from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import useSWR from 'swr';
|
||||
import type { TrackSearchResultDTO } from '@musicshare/shared';
|
||||
import type { ProfileDTO, TrackSearchResultDTO } from '@musicshare/shared';
|
||||
import { api } from '@/lib/api';
|
||||
import { showError } from '@/lib/errors';
|
||||
import { colors, radius, space } from '@/lib/theme';
|
||||
import { useSession } from '@/lib/session';
|
||||
import { useDebounced } from '@/lib/useDebounced';
|
||||
import { Body, Button, Card, Field, Heading } from './ui';
|
||||
import { Body, Button, Card, Field, Heading, Row } from './ui';
|
||||
|
||||
/**
|
||||
* Search for a song and pin it to your profile with an optional note.
|
||||
* `bare` drops the card and heading (for onboarding, which has its own title).
|
||||
*/
|
||||
export function FeaturedPicker({ bare, onSaved }: { bare?: boolean; onSaved?: () => void }) {
|
||||
const { me } = useSession();
|
||||
// In Settings, show what's featured now (with Change / Remove) before the search.
|
||||
const { data: profile, mutate: refreshProfile } = useSWR<ProfileDTO>(!bare && me ? `/users/${me.username}` : null);
|
||||
const current = profile?.featured ?? null;
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [q, setQ] = useState('');
|
||||
const [selected, setSelected] = useState<TrackSearchResultDTO | null>(null);
|
||||
const [comment, setComment] = useState('');
|
||||
@@ -31,6 +37,12 @@ export function FeaturedPicker({ bare, onSaved }: { bare?: boolean; onSaved?: ()
|
||||
await api.setFeatured({ deezerId: selected.deezerId, comment: comment.trim() || null });
|
||||
setSaved(true);
|
||||
onSaved?.();
|
||||
if (!bare) {
|
||||
setEditing(false);
|
||||
setSelected(null);
|
||||
setComment('');
|
||||
await refreshProfile();
|
||||
}
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
}
|
||||
@@ -86,12 +98,49 @@ export function FeaturedPicker({ bare, onSaved }: { bare?: boolean; onSaved?: ()
|
||||
);
|
||||
|
||||
if (bare) return <View style={{ gap: space.md }}>{body}</View>;
|
||||
|
||||
const remove = async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await api.clearFeatured();
|
||||
setSaved(false);
|
||||
await refreshProfile();
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
}
|
||||
setBusy(false);
|
||||
};
|
||||
|
||||
if (current && !editing) {
|
||||
return (
|
||||
<Card>
|
||||
<Heading>Featured song</Heading>
|
||||
<View style={styles.result}>
|
||||
{current.track.artworkUrl && <Image source={current.track.artworkUrl} style={styles.thumb} />}
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.title} numberOfLines={1}>
|
||||
{current.track.title}
|
||||
</Text>
|
||||
<Body muted style={styles.small}>
|
||||
{current.track.artist}
|
||||
</Body>
|
||||
</View>
|
||||
</View>
|
||||
{current.comment ? <Body style={styles.small}>“{current.comment}”</Body> : null}
|
||||
<Row>
|
||||
<Button small title="Change" onPress={() => (setEditing(true), setComment(current.comment ?? ''))} />
|
||||
<Button small title="Remove" loading={busy} onPress={remove} />
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card>
|
||||
<Heading>Featured song</Heading>
|
||||
<Body muted style={styles.small}>
|
||||
Pin a song to your profile, with an optional note about why.
|
||||
</Body>
|
||||
{current && <Button small title={`Keep ${current.track.title}`} onPress={() => setEditing(false)} style={{ alignSelf: 'flex-start' }} />}
|
||||
{body}
|
||||
</Card>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user