fix(channel): persist across reload, keep banner aspect, fix avatar z-order
- F5 on a channel page kept bouncing to the feed — the App init dropped history.state._chan like the (intentionally non-replayed) YouTube search. Now channelView restores from _chan on load and the stamp preserves it, so a reload stays on the channel page. - The banner was stretched/cropped (bg-cover). Render it as an <img object-contain> with a capped height, letterboxed on the surface colour, so its real aspect ratio is kept. - The round avatar's top was clipped by the banner: the banner's position:relative container painted above the static identity row. Give that row relative z-10 so the overlapping avatar sits on top.
This commit is contained in:
parent
e185e03c6c
commit
b4a1fa079b
2 changed files with 22 additions and 10 deletions
|
|
@ -145,7 +145,12 @@ export default function App() {
|
||||||
// browser-history entry (history.state._chan = channel id, _chanName = a display name to show
|
// browser-history entry (history.state._chan = channel id, _chanName = a display name to show
|
||||||
// before the detail loads), so Back closes the channel page (after any player opened over it)
|
// before the detail loads), so Back closes the channel page (after any player opened over it)
|
||||||
// and reload returns to the normal app. Channels/videos are reached by id, so it's not in the URL.
|
// and reload returns to the normal app. Channels/videos are reached by id, so it's not in the URL.
|
||||||
const [channelView, setChannelView] = useState<{ id: string; name?: string } | null>(null);
|
const [channelView, setChannelView] = useState<{ id: string; name?: string } | null>(() => {
|
||||||
|
// Restore the open channel page across a reload (it rides in history.state, which survives F5),
|
||||||
|
// unlike the YouTube search which is intentionally dropped (it would re-spend quota).
|
||||||
|
const c = window.history.state?._chan as string | undefined;
|
||||||
|
return c ? { id: c, name: (window.history.state?._chanName as string) ?? undefined } : null;
|
||||||
|
});
|
||||||
const openChannel = useCallback((id: string, name?: string) => {
|
const openChannel = useCallback((id: string, name?: string) => {
|
||||||
setChannelView({ id, name });
|
setChannelView({ id, name });
|
||||||
const st = window.history.state || {};
|
const st = window.history.state || {};
|
||||||
|
|
@ -264,8 +269,9 @@ export default function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Drop any stale _yt from a prior session (a reload starts on the normal feed; ytSearch
|
// Drop any stale _yt from a prior session (a reload starts on the normal feed; ytSearch
|
||||||
// begins null), so the first Back doesn't resurrect a search we're no longer showing.
|
// begins null), so the first Back doesn't resurrect a search we're no longer showing.
|
||||||
const { _yt: _staleYt, _chan: _staleChan, _chanName: _staleChanName, ...rest } =
|
// Drop a stale _yt (a reload starts on the normal feed; a search would re-spend quota), but
|
||||||
window.history.state || {};
|
// KEEP _chan/_chanName so the open channel page survives F5 (channelView restores from it above).
|
||||||
|
const { _yt: _staleYt, ...rest } = window.history.state || {};
|
||||||
window.history.replaceState({ ...rest, sfPage: page }, "");
|
window.history.replaceState({ ...rest, sfPage: page }, "");
|
||||||
function onPop(e: PopStateEvent) {
|
function onPop(e: PopStateEvent) {
|
||||||
const p = (e.state?.sfPage as Page) ?? "feed";
|
const p = (e.state?.sfPage as Page) ?? "feed";
|
||||||
|
|
|
||||||
|
|
@ -133,16 +133,21 @@ export default function ChannelPage({
|
||||||
{/* Banner + back */}
|
{/* Banner + back */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{ch?.banner_url ? (
|
{ch?.banner_url ? (
|
||||||
<div
|
// Keep the banner's real aspect ratio (object-contain, capped height); letterbox with
|
||||||
className="h-24 sm:h-36 w-full bg-cover bg-center"
|
// the surface colour rather than stretching/cropping it to fill.
|
||||||
style={{ backgroundImage: `url(${ch.banner_url})` }}
|
<div className="w-full flex justify-center bg-surface">
|
||||||
|
<img
|
||||||
|
src={ch.banner_url}
|
||||||
|
alt=""
|
||||||
|
className="block max-h-40 w-auto max-w-full object-contain"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-14 w-full bg-surface" />
|
<div className="h-14 w-full bg-surface" />
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="absolute top-3 left-3 inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm bg-black/45 text-white hover:bg-black/65 backdrop-blur-sm"
|
className="absolute top-3 left-3 z-10 inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm bg-black/45 text-white hover:bg-black/65 backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-4 h-4" />
|
<ArrowLeft className="w-4 h-4" />
|
||||||
{t("channel.back")}
|
{t("channel.back")}
|
||||||
|
|
@ -150,8 +155,9 @@ export default function ChannelPage({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-4 sm:px-6">
|
<div className="px-4 sm:px-6">
|
||||||
{/* Identity + actions */}
|
{/* Identity + actions. relative+z-10 so the avatar that overlaps up into the banner
|
||||||
<div className="flex items-end gap-4 -mt-8">
|
paints ABOVE it (the banner's positioned container would otherwise cover it). */}
|
||||||
|
<div className="relative z-10 flex items-end gap-4 -mt-8">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={ch?.thumbnail_url ?? null}
|
src={ch?.thumbnail_url ?? null}
|
||||||
fallback={name}
|
fallback={name}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue