This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

refactor: semble header

+189 -171
+3 -4
src/webapp/features/cards/lib/queries/useUrlMetadata.tsx
··· 17 17 type Props = PropsWithStats | PropsWithoutStats; 18 18 19 19 export default function useUrlMetadata(props: Props) { 20 - // Use regular useQuery when we have initialData (with stats) 21 - // Use useSuspenseQuery for basic metadata queries without stats 22 - if (props.includeStats && 'initialData' in props) { 20 + if (props.includeStats) { 21 + // Non-suspense: stats are progressive — tabs render immediately, counts fill in async 23 22 return useQuery({ 24 23 queryKey: cardKeys.urlMetadata(props.url, { includeStats: true }), 25 24 queryFn: () => getUrlMetadata({ url: props.url, includeStats: true }), 26 - initialData: props.initialData, 25 + initialData: 'initialData' in props ? props.initialData : undefined, 27 26 }); 28 27 } 29 28
+7 -34
src/webapp/features/platforms/bluesky/components/blueskySemblePost/BlueskySemblePost.tsx
··· 3 3 import SembleHeader from '@/features/semble/components/SembleHeader/SembleHeader'; 4 4 import { getPostUriFromUrl } from '@/lib/utils/atproto'; 5 5 import RichTextRenderer from '@/components/contentDisplay/richTextRenderer/RichTextRenderer'; 6 - import { 7 - detectUrlPlatform, 8 - getDomain, 9 - SupportedPlatform, 10 - } from '@/lib/utils/link'; 6 + import { detectUrlPlatform, getDomain, SupportedPlatform } from '@/lib/utils/link'; 11 7 import { getFormattedDate } from '@/lib/utils/time'; 12 8 import { 13 9 Stack, ··· 19 15 Box, 20 16 Image, 21 17 Text, 22 - Badge, 23 18 } from '@mantine/core'; 24 19 import Link from 'next/link'; 25 20 import { FaBluesky } from 'react-icons/fa6'; 26 21 import PostEmbed from '../postEmbed/PostEmbed'; 27 22 import BlackskyLogo from '@/assets/icons/blacksky-logo.svg'; 28 23 import BlackskyLogoWhite from '@/assets/icons/blacksky-logo-white.svg'; 29 - import { RiArrowRightUpLine } from 'react-icons/ri'; 30 - import { UrlType } from '@semble/types'; 31 - import { getUrlTypeIcon } from '@/lib/utils/icon'; 32 - import { IconType } from 'react-icons/lib'; 33 - import { getUrlMetadata } from '@/features/cards/lib/dal'; 24 + import { Suspense } from 'react'; 25 + import UrlTypeBadge from '@/features/semble/components/urlTypeBadge/UrlTypeBadge'; 26 + import UrlTypeBadgeSkeleton from '@/features/semble/components/urlTypeBadge/Skeleton.UrlTypeBadge'; 34 27 35 28 interface Props { 36 29 url: string; ··· 39 32 export default async function BlueskySemblePost(props: Props) { 40 33 const postUri = getPostUriFromUrl(props.url); 41 34 const data = await getBlueskyPost(postUri); 42 - const { metadata } = await getUrlMetadata({ url: props.url }); 43 - const urlTypeIcon = getUrlTypeIcon(metadata.type as UrlType); 44 - const IconComponent = urlTypeIcon as IconType; 45 35 46 36 const platform = detectUrlPlatform(props.url); 47 37 const platformIcon = ··· 81 71 82 72 return ( 83 73 <Stack gap={'xs'}> 84 - <Group gap={'xs'}> 85 - <Badge size="xs" color="lime" leftSection={<IconComponent />}> 86 - {metadata.type} 87 - </Badge> 88 - <Tooltip label={props.url}> 89 - <Anchor 90 - component={Link} 91 - target="_blank" 92 - fw={700} 93 - c="blue" 94 - href={props.url} 95 - style={{ display: 'inline-block' }} 96 - > 97 - <Group gap={0} align="center" wrap="nowrap"> 98 - {getDomain(props.url)} 99 - <RiArrowRightUpLine /> 100 - </Group> 101 - </Anchor> 102 - </Tooltip> 103 - </Group> 74 + <Suspense fallback={<UrlTypeBadgeSkeleton />}> 75 + <UrlTypeBadge url={props.url} /> 76 + </Suspense> 104 77 105 78 {/* Post */} 106 79 <Card radius={'lg'} withBorder>
+1 -1
src/webapp/features/platforms/bluesky/container/blueskySembleHeader/BlueskySembleHeader.tsx
··· 14 14 15 15 export default async function BlueskySembleHeader(props: Props) { 16 16 return ( 17 - <Stack gap={'sm'} mx={'auto'} w={'100%'}> 17 + <Stack gap={'sm'} mx={'auto'} w={'100%'} maw={600}> 18 18 <Suspense fallback={<BlueskySemblePostSkeleton />}> 19 19 <BlueskySemblePost url={props.url} /> 20 20 </Suspense>
+24 -11
src/webapp/features/semble/components/SembleHeader/SembleHeader.tsx
··· 1 - import { Stack, Grid } from '@mantine/core'; 1 + import { Stack, Grid, GridCol } from '@mantine/core'; 2 2 import UrlAddedBySummary from '../urlAddedBySummary/UrlAddedBySummary'; 3 3 import { Suspense } from 'react'; 4 4 import SembleActionsContainerSkeleton from '../../containers/sembleActionsContainer/Skeleton.SembleActionsContainer'; 5 5 import SembleActionsContainer from '../../containers/sembleActionsContainer/SembleActionsContainer'; 6 6 import UrlMetadataHeader from '../urlMetadataHeader/UrlMetadataHeader'; 7 + import UrlMetadataHeaderSkeleton from '../urlMetadataHeader/Skeleton.UrlMetadataHeader'; 7 8 import UrlAddedBySummarySkeleton from '../urlAddedBySummary/Skeleton.UrlAddedBySummary'; 9 + import UrlMetadataImage from '../urlMetadataHeader/UrlMetadataImage'; 10 + import UrlMetadataImageSkeleton from '../urlMetadataHeader/Skeleton.UrlMetadataImage'; 8 11 9 12 interface Props { 10 13 url: string; 11 14 viaCardId?: string; 12 15 } 13 16 14 - export default async function SembleHeader(props: Props) { 17 + export default function SembleHeader(props: Props) { 15 18 return ( 16 19 <Stack gap={'xl'}> 17 - <UrlMetadataHeader url={props.url}> 18 - <Stack align="center"> 19 - <Suspense fallback={<SembleActionsContainerSkeleton />}> 20 - <SembleActionsContainer 21 - url={props.url} 22 - viaCardId={props.viaCardId} 23 - /> 20 + <Grid gutter="lg" justify="space-between"> 21 + <GridCol span={{ base: 'auto' }}> 22 + <Suspense fallback={<UrlMetadataHeaderSkeleton />}> 23 + <UrlMetadataHeader url={props.url} /> 24 24 </Suspense> 25 - </Stack> 26 - </UrlMetadataHeader> 25 + </GridCol> 26 + <GridCol span={{ base: 12, sm: 'content' }}> 27 + <Stack gap={'sm'} align="center"> 28 + <Suspense fallback={<UrlMetadataImageSkeleton />}> 29 + <UrlMetadataImage url={props.url} /> 30 + </Suspense> 31 + <Suspense fallback={<SembleActionsContainerSkeleton />}> 32 + <SembleActionsContainer 33 + url={props.url} 34 + viaCardId={props.viaCardId} 35 + /> 36 + </Suspense> 37 + </Stack> 38 + </GridCol> 39 + </Grid> 27 40 28 41 <Suspense fallback={<UrlAddedBySummarySkeleton />}> 29 42 <UrlAddedBySummary url={props.url} />
+5
src/webapp/features/semble/components/urlMetadataHeader/Skeleton.UrlMetadataImage.tsx
··· 1 + import { Skeleton } from '@mantine/core'; 2 + 3 + export default function UrlMetadataImageSkeleton() { 4 + return <Skeleton w={'100%'} h={150} radius={'md'} />; 5 + }
+26 -97
src/webapp/features/semble/components/urlMetadataHeader/UrlMetadataHeader.tsx
··· 1 1 import { getUrlMetadata } from '@/features/cards/lib/dal'; 2 - import { getUrlTypeIcon } from '@/lib/utils/icon'; 3 - import { getDomain } from '@/lib/utils/link'; 4 - import { 5 - Stack, 6 - Tooltip, 7 - Anchor, 8 - Image, 9 - Title, 10 - Text, 11 - Spoiler, 12 - Card, 13 - Grid, 14 - GridCol, 15 - Group, 16 - Badge, 17 - } from '@mantine/core'; 18 - import { UrlType } from '@semble/types'; 2 + import { Stack, Anchor, Title, Text, Spoiler } from '@mantine/core'; 19 3 import Link from 'next/link'; 20 - import { IconType } from 'react-icons/lib'; 21 - import { RiArrowRightUpLine } from 'react-icons/ri'; 4 + import UrlTypeBadge from '../urlTypeBadge/UrlTypeBadge'; 22 5 23 6 interface Props { 24 7 url: string; 25 - children: React.ReactNode; 26 8 } 27 9 28 10 export default async function UrlMetadataHeader(props: Props) { 29 - const { metadata } = await getUrlMetadata({ 30 - url: props.url, 31 - includeStats: true, 32 - }); 33 - const urlTypeIcon = getUrlTypeIcon(metadata.type as UrlType); 34 - const IconComponent = urlTypeIcon as IconType; 11 + const { metadata } = await getUrlMetadata({ url: props.url }); 35 12 36 13 return ( 37 - <Grid gutter={'lg'} justify="space-between"> 38 - <GridCol span={{ base: 'auto' }}> 39 - <Stack> 40 - <Stack gap={0} align="start"> 41 - <Group gap={'xs'}> 42 - <Badge size="xs" color="lime" leftSection={<IconComponent />}> 43 - {metadata.type} 44 - </Badge> 45 - <Tooltip label={metadata.url}> 46 - <Anchor 47 - component={Link} 48 - target="_blank" 49 - fw={700} 50 - c="blue" 51 - href={props.url} 52 - style={{ display: 'inline-block' }} 53 - > 54 - <Group gap={0} align="center" wrap="nowrap"> 55 - {getDomain(props.url)} 56 - <RiArrowRightUpLine /> 57 - </Group> 58 - </Anchor> 59 - </Tooltip> 60 - </Group> 61 - 62 - {metadata.title && ( 63 - <Anchor 64 - component={Link} 65 - href={metadata.url} 66 - target="_blank" 67 - c={'inherit'} 68 - w={'fit-content'} 69 - > 70 - <Title order={1}>{metadata.title}</Title> 71 - </Anchor> 72 - )} 73 - </Stack> 74 - {metadata.description && ( 75 - <Spoiler showLabel={'Read more'} hideLabel={'See less'}> 76 - <Text c="gray" fw={500} maw={650}> 77 - {metadata.description} 78 - </Text> 79 - </Spoiler> 80 - )} 81 - </Stack> 82 - </GridCol> 83 - <GridCol span={{ base: 12, sm: 'content' }}> 84 - <Stack gap={'sm'} align="center"> 85 - {metadata.imageUrl && ( 86 - <Card 87 - p={0} 88 - radius={'md'} 89 - component={Link} 90 - href={props.url} 91 - target="_blank" 92 - withBorder 93 - > 94 - <Image 95 - src={metadata.imageUrl} 96 - alt={`${props.url} social preview image`} 97 - mah={150} 98 - w={'auto'} 99 - maw={'100%'} 100 - fit="contain" 101 - /> 102 - </Card> 103 - )} 104 - {props.children} 105 - </Stack> 106 - </GridCol> 107 - </Grid> 14 + <Stack> 15 + <Stack gap={0} align="start"> 16 + <UrlTypeBadge url={props.url} /> 17 + {metadata.title && ( 18 + <Anchor 19 + component={Link} 20 + href={metadata.url} 21 + target="_blank" 22 + c={'inherit'} 23 + w={'fit-content'} 24 + > 25 + <Title order={1}>{metadata.title}</Title> 26 + </Anchor> 27 + )} 28 + </Stack> 29 + {metadata.description && ( 30 + <Spoiler showLabel={'Read more'} hideLabel={'See less'}> 31 + <Text c="gray" fw={500} maw={650}> 32 + {metadata.description} 33 + </Text> 34 + </Spoiler> 35 + )} 36 + </Stack> 108 37 ); 109 38 }
+33
src/webapp/features/semble/components/urlMetadataHeader/UrlMetadataImage.tsx
··· 1 + import { getUrlMetadata } from '@/features/cards/lib/dal'; 2 + import { Image, Card } from '@mantine/core'; 3 + import Link from 'next/link'; 4 + 5 + interface Props { 6 + url: string; 7 + } 8 + 9 + export default async function UrlMetadataImage(props: Props) { 10 + const { metadata } = await getUrlMetadata({ url: props.url }); 11 + 12 + if (!metadata.imageUrl) return null; 13 + 14 + return ( 15 + <Card 16 + p={0} 17 + radius={'md'} 18 + component={Link} 19 + href={props.url} 20 + target="_blank" 21 + withBorder 22 + > 23 + <Image 24 + src={metadata.imageUrl} 25 + alt={`${props.url} social preview image`} 26 + mah={150} 27 + w={'auto'} 28 + maw={'100%'} 29 + fit="contain" 30 + /> 31 + </Card> 32 + ); 33 + }
+5
src/webapp/features/semble/components/urlTypeBadge/Skeleton.UrlTypeBadge.tsx
··· 1 + import { Skeleton } from '@mantine/core'; 2 + 3 + export default function UrlTypeBadgeSkeleton() { 4 + return <Skeleton w={160} h={20} />; 5 + }
+35
src/webapp/features/semble/components/urlTypeBadge/UrlTypeBadge.tsx
··· 1 + import { getDomain } from '@/lib/utils/link'; 2 + import { Tooltip, Anchor, Group, Skeleton } from '@mantine/core'; 3 + import Link from 'next/link'; 4 + import { RiArrowRightUpLine } from 'react-icons/ri'; 5 + import { Suspense } from 'react'; 6 + import UrlTypeBadgeContent from './UrlTypeBadgeContent'; 7 + 8 + interface Props { 9 + url: string; 10 + } 11 + 12 + export default function UrlTypeBadge({ url }: Props) { 13 + return ( 14 + <Group gap={'xs'}> 15 + <Suspense fallback={<Skeleton w={60} h={20} radius={'xl'} />}> 16 + <UrlTypeBadgeContent url={url} /> 17 + </Suspense> 18 + <Tooltip label={url}> 19 + <Anchor 20 + component={Link} 21 + target="_blank" 22 + fw={700} 23 + c="blue" 24 + href={url} 25 + style={{ display: 'inline-block' }} 26 + > 27 + <Group gap={0} align="center" wrap="nowrap"> 28 + {getDomain(url)} 29 + <RiArrowRightUpLine /> 30 + </Group> 31 + </Anchor> 32 + </Tooltip> 33 + </Group> 34 + ); 35 + }
+21
src/webapp/features/semble/components/urlTypeBadge/UrlTypeBadgeContent.tsx
··· 1 + import { getUrlMetadata } from '@/features/cards/lib/dal'; 2 + import { getUrlTypeIcon } from '@/lib/utils/icon'; 3 + import { Badge } from '@mantine/core'; 4 + import { UrlType } from '@semble/types'; 5 + import { IconType } from 'react-icons/lib'; 6 + 7 + interface Props { 8 + url: string; 9 + } 10 + 11 + export default async function UrlTypeBadgeContent({ url }: Props) { 12 + const { metadata } = await getUrlMetadata({ url }); 13 + const urlTypeIcon = getUrlTypeIcon(metadata.type as UrlType); 14 + const IconComponent = urlTypeIcon as IconType; 15 + 16 + return ( 17 + <Badge size="xs" color="lime" leftSection={<IconComponent />}> 18 + {metadata.type} 19 + </Badge> 20 + ); 21 + }
+29 -24
src/webapp/features/semble/containers/sembleContainer/SembleContainer.tsx
··· 1 1 import SembleHeader from '../../components/SembleHeader/SembleHeader'; 2 - import { Box, Container, Stack } from '@mantine/core'; 2 + import { Container, Stack } from '@mantine/core'; 3 3 import { Suspense } from 'react'; 4 4 import SembleTabs from '../../components/sembleTabs/SembleTabs'; 5 5 import SembleHeaderSkeleton from '../../components/SembleHeader/Skeleton.SembleHeader'; ··· 7 7 import BlueskySembleHeader from '@/features/platforms/bluesky/container/blueskySembleHeader/BlueskySembleHeader'; 8 8 import { detectUrlPlatform, SupportedPlatform } from '@/lib/utils/link'; 9 9 import BlueskySembleHeaderSkeleton from '@/features/platforms/bluesky/container/blueskySembleHeader/Skeleton.BlueskySembleHeader'; 10 + import React from 'react'; 10 11 11 12 interface Props { 12 13 url: string; 13 14 viaCardId?: string; 14 15 } 15 16 17 + type PlatformEntry = { 18 + component: React.ComponentType<{ url: string; viaCardId?: string }>; 19 + skeleton: React.ComponentType; 20 + }; 21 + 22 + const PLATFORM_REGISTRY: Partial<Record<SupportedPlatform, PlatformEntry>> = { 23 + [SupportedPlatform.BLUESKY_POST]: { 24 + component: BlueskySembleHeader, 25 + skeleton: BlueskySembleHeaderSkeleton, 26 + }, 27 + [SupportedPlatform.BLACKSKY_POST]: { 28 + component: BlueskySembleHeader, 29 + skeleton: BlueskySembleHeaderSkeleton, 30 + }, 31 + }; 32 + 33 + const DEFAULT_ENTRY: PlatformEntry = { 34 + component: SembleHeader, 35 + skeleton: SembleHeaderSkeleton, 36 + }; 37 + 16 38 export default function SembleContainer(props: Props) { 17 39 const platform = detectUrlPlatform(props.url); 40 + const { component: HeaderComponent, skeleton: SkeletonComponent } = 41 + PLATFORM_REGISTRY[platform.type] ?? DEFAULT_ENTRY; 18 42 19 43 return ( 20 44 <Container p={0} fluid> 21 45 <SembleHeaderBackground /> 22 46 <Container px={'xs'} pb={'xs'} size={'xl'}> 23 47 <Stack gap={'xl'}> 24 - {platform.type === SupportedPlatform.BLUESKY_POST || 25 - platform.type === SupportedPlatform.BLACKSKY_POST ? ( 26 - <Box maw={600} w={'100%'} mx={'auto'}> 27 - <Suspense 28 - fallback={<BlueskySembleHeaderSkeleton />} 29 - key={props.url + 'bluesky header'} 30 - > 31 - <BlueskySembleHeader 32 - url={props.url} 33 - viaCardId={props.viaCardId} 34 - /> 35 - </Suspense> 36 - </Box> 37 - ) : ( 38 - <Suspense 39 - fallback={<SembleHeaderSkeleton />} 40 - key={props.url + 'semble header'} 41 - > 42 - <SembleHeader url={props.url} viaCardId={props.viaCardId} /> 43 - </Suspense> 44 - )} 45 - <Suspense> 46 - <SembleTabs url={props.url} /> 48 + <Suspense fallback={<SkeletonComponent />} key={props.url + 'header'}> 49 + <HeaderComponent url={props.url} viaCardId={props.viaCardId} /> 47 50 </Suspense> 51 + 52 + <SembleTabs url={props.url} /> 48 53 </Stack> 49 54 </Container> 50 55 </Container>