Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client';

import React, { useEffect, useState, useCallback } from 'react';
import { Button, Empty } from 'antd';
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import { Button, Empty, Badge } from 'antd';
import { LeftOutlined, RightOutlined, InstagramOutlined } from '@ant-design/icons';
import { useSwipeable } from 'react-swipeable';
import { useRouter, useSearchParams } from 'next/navigation';
import { track } from '@vercel/analytics';
Expand All @@ -12,6 +12,8 @@ import BoardRenderer from '@/app/components/board-renderer/board-renderer';
import ClimbTitle from '@/app/components/climb-card/climb-title';
import { constructClimbListWithSlugs, constructPlayUrlWithSlugs } from '@/app/lib/url-utils';
import { themeTokens } from '@/app/theme/theme-config';
import InstagramDrawer from '@/app/components/instagram-drawer/instagram-drawer';
import { useBetaCount } from '@/app/components/instagram-drawer/use-beta-count';
import styles from './play-view.module.css';

type PlayViewClientProps = {
Expand All @@ -37,10 +39,16 @@ const PlayViewClient: React.FC<PlayViewClientProps> = ({ boardDetails, initialCl

const [swipeOffset, setSwipeOffset] = useState(0);
const [showSwipeHint, setShowSwipeHint] = useState(true);
const [isInstagramDrawerOpen, setIsInstagramDrawerOpen] = useState(false);

// Use queue's current climb if available, otherwise use initial climb from SSR
const displayClimb = currentClimb || initialClimb;

const betaCount = useBetaCount({
climbUuid: displayClimb?.uuid,
boardName: boardDetails.board_name,
});

// Hide swipe hint after first interaction
useEffect(() => {
const timer = setTimeout(() => {
Expand Down Expand Up @@ -177,9 +185,29 @@ const PlayViewClient: React.FC<PlayViewClientProps> = ({ boardDetails, initialCl
className={styles.climbTitleContainer}
style={{
padding: `${themeTokens.spacing[1]}px ${themeTokens.spacing[3]}px`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: themeTokens.spacing[2],
}}
>
<ClimbTitle climb={displayClimb} layout="horizontal" showSetterInfo />
<div style={{ flex: 1, minWidth: 0 }}>
<ClimbTitle climb={displayClimb} layout="horizontal" showSetterInfo />
</div>
<Badge count={betaCount ?? 0} size="small" offset={[-4, 4]}>
<Button
type="text"
icon={<InstagramOutlined />}
onClick={() => {
setIsInstagramDrawerOpen(true);
track('Instagram Drawer Opened', {
source: 'play-screen',
boardLayout: boardDetails.layout_name || '',
});
}}
aria-label="View beta videos"
/>
</Badge>
</div>
<div {...swipeHandlers} className={styles.swipeContainer}>
{/* Swipe indicators */}
Expand Down Expand Up @@ -217,6 +245,14 @@ const PlayViewClient: React.FC<PlayViewClientProps> = ({ boardDetails, initialCl
)}
</div>
</div>

{/* Instagram Drawer for Beta Videos */}
<InstagramDrawer
open={isInstagramDrawerOpen}
onClose={() => setIsInstagramDrawerOpen(false)}
climb={displayClimb}
boardName={boardDetails.board_name}
/>
</div>
);
};
Expand Down
244 changes: 244 additions & 0 deletions packages/web/app/components/instagram-drawer/instagram-drawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
'use client';

import React, { useState } from 'react';
import { Drawer, Card, Row, Col, Typography, Empty, Modal, Spin } from 'antd';
import { InstagramOutlined, UserOutlined } from '@ant-design/icons';
import { BetaLink } from '@/app/lib/api-wrappers/sync-api-types';
import { BoardName, Climb } from '@/app/lib/types';
import { themeTokens } from '@/app/theme/theme-config';
import { useBetaLinks } from './use-beta-links';

const { Text } = Typography;

interface InstagramDrawerProps {
open: boolean;
onClose: () => void;
climb: Climb | null;
boardName: BoardName;
}

const getInstagramEmbedUrl = (link: string) => {
const instagramRegex = /(?:instagram\.com|instagr\.am)\/(?:p|reel|tv)\/([\w-]+)/;
const match = link.match(instagramRegex);

if (match && match[1]) {
return `https://www.instagram.com/p/${match[1]}/embed`;
}

return null;
};

const InstagramDrawer: React.FC<InstagramDrawerProps> = ({ open, onClose, climb, boardName }) => {
const [modalVisible, setModalVisible] = useState(false);
const [selectedVideo, setSelectedVideo] = useState<BetaLink | null>(null);
const [iframeKey, setIframeKey] = useState(0);

const { betaLinks, loading, error } = useBetaLinks({
climbUuid: climb?.uuid,
boardName,
enabled: open,
});

const handleVideoClick = (betaLink: BetaLink) => {
setSelectedVideo(betaLink);
setModalVisible(true);
};

const handleModalClose = () => {
setIframeKey((prev) => prev + 1);
setModalVisible(false);
setSelectedVideo(null);
};

const renderContent = () => {
if (loading) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 200 }}>
<Spin size="large" />
</div>
);
}

if (error) {
return <Empty description={error} image={Empty.PRESENTED_IMAGE_SIMPLE} />;
}

if (betaLinks.length === 0) {
return <Empty description="No beta videos available for this climb" image={Empty.PRESENTED_IMAGE_SIMPLE} />;
}

return (
<Row gutter={[12, 12]}>
{betaLinks.map((betaLink, index) => {
const embedUrl = getInstagramEmbedUrl(betaLink.link);

return (
<Col xs={24} sm={12} key={betaLink.link}>
<Card
hoverable
size="small"
styles={{ body: { padding: 0 } }}
onClick={() => handleVideoClick(betaLink)}
>
{embedUrl ? (
<div
style={{
position: 'relative',
paddingBottom: '100%',
overflow: 'hidden',
borderRadius: `${themeTokens.borderRadius.md}px ${themeTokens.borderRadius.md}px 0 0`,
}}
>
<iframe
src={embedUrl}
style={{
position: 'absolute',
top: '-20%',
left: 0,
width: '100%',
height: '140%',
border: 'none',
pointerEvents: 'none',
}}
scrolling="no"
title={`Beta video ${index + 1} thumbnail`}
/>
</div>
) : (
<div
style={{
padding: themeTokens.spacing[8],
textAlign: 'center',
background: themeTokens.neutral[100],
}}
>
<InstagramOutlined style={{ fontSize: 32, color: themeTokens.neutral[400] }} />
<p style={{ margin: `${themeTokens.spacing[2]}px 0 0`, color: themeTokens.neutral[500] }}>
Unable to load video
</p>
</div>
)}
<div
style={{
padding: themeTokens.spacing[3],
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
borderTop: `1px solid ${themeTokens.neutral[100]}`,
}}
>
{betaLink.foreign_username && (
<Text type="secondary" style={{ fontSize: themeTokens.typography.fontSize.sm }}>
<UserOutlined style={{ marginRight: 4 }} />@{betaLink.foreign_username}
{betaLink.angle && <span style={{ marginLeft: 8 }}>{betaLink.angle}°</span>}
</Text>
)}
<a
href={betaLink.link}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
style={{
color: themeTokens.colors.primary,
fontSize: themeTokens.typography.fontSize.sm,
display: 'flex',
alignItems: 'center',
gap: 4,
}}
>
<InstagramOutlined /> View
</a>
</div>
</Card>
</Col>
);
})}
</Row>
);
};

return (
<>
<Drawer
title={
<div style={{ display: 'flex', alignItems: 'center', gap: themeTokens.spacing[2] }}>
<InstagramOutlined style={{ color: themeTokens.colors.primary }} />
<span>Beta Videos</span>
{climb?.name && (
<Text type="secondary" style={{ fontWeight: 'normal', fontSize: themeTokens.typography.fontSize.sm }}>
- {climb.name}
</Text>
)}
</div>
}
placement="bottom"
height="90%"
open={open}
onClose={onClose}
styles={{
body: {
padding: themeTokens.spacing[4],
overflow: 'auto',
},
}}
>
{renderContent()}
</Drawer>

{modalVisible && (
<Modal
title={selectedVideo?.foreign_username ? `Beta by @${selectedVideo.foreign_username}` : 'Beta Video'}
open={modalVisible}
onCancel={handleModalClose}
footer={
<a
href={selectedVideo?.link}
target="_blank"
rel="noopener noreferrer"
style={{
color: themeTokens.colors.primary,
display: 'inline-flex',
alignItems: 'center',
gap: 6,
}}
>
<InstagramOutlined /> View on Instagram
</a>
}
width="90%"
style={{ maxWidth: '500px' }}
centered
destroyOnClose
>
{selectedVideo && (
<div
style={{
position: 'relative',
paddingBottom: '140%',
overflow: 'hidden',
borderRadius: themeTokens.borderRadius.md,
}}
>
<iframe
key={iframeKey}
src={getInstagramEmbedUrl(selectedVideo.link) || ''}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
border: 'none',
}}
scrolling="no"
title="Beta video"
/>
</div>
)}
</Modal>
)}
</>
);
};

export default InstagramDrawer;
15 changes: 15 additions & 0 deletions packages/web/app/components/instagram-drawer/use-beta-count.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use client';

import { BoardName } from '@/app/lib/types';
import { useBetaLinks } from './use-beta-links';

interface UseBetaCountOptions {
climbUuid: string | undefined;
boardName: BoardName;
enabled?: boolean;
}

export function useBetaCount({ climbUuid, boardName, enabled = true }: UseBetaCountOptions): number | null {
const { count } = useBetaLinks({ climbUuid, boardName, enabled });
return count;
}
Loading
Loading