src
webapp
features
cards
platforms
semble
components
SembleHeader
urlMetadataHeader
urlTypeBadge
containers
···
17
17
type Props = PropsWithStats | PropsWithoutStats;
18
18
19
19
export default function useUrlMetadata(props: Props) {
20
20
-
// Use regular useQuery when we have initialData (with stats)
21
21
-
// Use useSuspenseQuery for basic metadata queries without stats
22
22
-
if (props.includeStats && 'initialData' in props) {
20
20
+
if (props.includeStats) {
21
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
26
-
initialData: props.initialData,
25
25
+
initialData: 'initialData' in props ? props.initialData : undefined,
27
26
});
28
27
}
29
28
···
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
6
-
import {
7
7
-
detectUrlPlatform,
8
8
-
getDomain,
9
9
-
SupportedPlatform,
10
10
-
} from '@/lib/utils/link';
6
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
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
29
-
import { RiArrowRightUpLine } from 'react-icons/ri';
30
30
-
import { UrlType } from '@semble/types';
31
31
-
import { getUrlTypeIcon } from '@/lib/utils/icon';
32
32
-
import { IconType } from 'react-icons/lib';
33
33
-
import { getUrlMetadata } from '@/features/cards/lib/dal';
24
24
+
import { Suspense } from 'react';
25
25
+
import UrlTypeBadge from '@/features/semble/components/urlTypeBadge/UrlTypeBadge';
26
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
42
-
const { metadata } = await getUrlMetadata({ url: props.url });
43
43
-
const urlTypeIcon = getUrlTypeIcon(metadata.type as UrlType);
44
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
84
-
<Group gap={'xs'}>
85
85
-
<Badge size="xs" color="lime" leftSection={<IconComponent />}>
86
86
-
{metadata.type}
87
87
-
</Badge>
88
88
-
<Tooltip label={props.url}>
89
89
-
<Anchor
90
90
-
component={Link}
91
91
-
target="_blank"
92
92
-
fw={700}
93
93
-
c="blue"
94
94
-
href={props.url}
95
95
-
style={{ display: 'inline-block' }}
96
96
-
>
97
97
-
<Group gap={0} align="center" wrap="nowrap">
98
98
-
{getDomain(props.url)}
99
99
-
<RiArrowRightUpLine />
100
100
-
</Group>
101
101
-
</Anchor>
102
102
-
</Tooltip>
103
103
-
</Group>
74
74
+
<Suspense fallback={<UrlTypeBadgeSkeleton />}>
75
75
+
<UrlTypeBadge url={props.url} />
76
76
+
</Suspense>
104
77
105
78
{/* Post */}
106
79
<Card radius={'lg'} withBorder>
···
1
1
+
import { Skeleton } from '@mantine/core';
2
2
+
3
3
+
export default function UrlTypeBadgeSkeleton() {
4
4
+
return <Skeleton w={160} h={20} />;
5
5
+
}
···
1
1
+
import { getDomain } from '@/lib/utils/link';
2
2
+
import { Tooltip, Anchor, Group, Skeleton } from '@mantine/core';
3
3
+
import Link from 'next/link';
4
4
+
import { RiArrowRightUpLine } from 'react-icons/ri';
5
5
+
import { Suspense } from 'react';
6
6
+
import UrlTypeBadgeContent from './UrlTypeBadgeContent';
7
7
+
8
8
+
interface Props {
9
9
+
url: string;
10
10
+
}
11
11
+
12
12
+
export default function UrlTypeBadge({ url }: Props) {
13
13
+
return (
14
14
+
<Group gap={'xs'}>
15
15
+
<Suspense fallback={<Skeleton w={60} h={20} radius={'xl'} />}>
16
16
+
<UrlTypeBadgeContent url={url} />
17
17
+
</Suspense>
18
18
+
<Tooltip label={url}>
19
19
+
<Anchor
20
20
+
component={Link}
21
21
+
target="_blank"
22
22
+
fw={700}
23
23
+
c="blue"
24
24
+
href={url}
25
25
+
style={{ display: 'inline-block' }}
26
26
+
>
27
27
+
<Group gap={0} align="center" wrap="nowrap">
28
28
+
{getDomain(url)}
29
29
+
<RiArrowRightUpLine />
30
30
+
</Group>
31
31
+
</Anchor>
32
32
+
</Tooltip>
33
33
+
</Group>
34
34
+
);
35
35
+
}
···
1
1
+
import { getUrlMetadata } from '@/features/cards/lib/dal';
2
2
+
import { getUrlTypeIcon } from '@/lib/utils/icon';
3
3
+
import { Badge } from '@mantine/core';
4
4
+
import { UrlType } from '@semble/types';
5
5
+
import { IconType } from 'react-icons/lib';
6
6
+
7
7
+
interface Props {
8
8
+
url: string;
9
9
+
}
10
10
+
11
11
+
export default async function UrlTypeBadgeContent({ url }: Props) {
12
12
+
const { metadata } = await getUrlMetadata({ url });
13
13
+
const urlTypeIcon = getUrlTypeIcon(metadata.type as UrlType);
14
14
+
const IconComponent = urlTypeIcon as IconType;
15
15
+
16
16
+
return (
17
17
+
<Badge size="xs" color="lime" leftSection={<IconComponent />}>
18
18
+
{metadata.type}
19
19
+
</Badge>
20
20
+
);
21
21
+
}
···
1
1
import SembleHeader from '../../components/SembleHeader/SembleHeader';
2
2
-
import { Box, Container, Stack } from '@mantine/core';
2
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
10
+
import React from 'react';
10
11
11
12
interface Props {
12
13
url: string;
13
14
viaCardId?: string;
14
15
}
15
16
17
17
+
type PlatformEntry = {
18
18
+
component: React.ComponentType<{ url: string; viaCardId?: string }>;
19
19
+
skeleton: React.ComponentType;
20
20
+
};
21
21
+
22
22
+
const PLATFORM_REGISTRY: Partial<Record<SupportedPlatform, PlatformEntry>> = {
23
23
+
[SupportedPlatform.BLUESKY_POST]: {
24
24
+
component: BlueskySembleHeader,
25
25
+
skeleton: BlueskySembleHeaderSkeleton,
26
26
+
},
27
27
+
[SupportedPlatform.BLACKSKY_POST]: {
28
28
+
component: BlueskySembleHeader,
29
29
+
skeleton: BlueskySembleHeaderSkeleton,
30
30
+
},
31
31
+
};
32
32
+
33
33
+
const DEFAULT_ENTRY: PlatformEntry = {
34
34
+
component: SembleHeader,
35
35
+
skeleton: SembleHeaderSkeleton,
36
36
+
};
37
37
+
16
38
export default function SembleContainer(props: Props) {
17
39
const platform = detectUrlPlatform(props.url);
40
40
+
const { component: HeaderComponent, skeleton: SkeletonComponent } =
41
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
24
-
{platform.type === SupportedPlatform.BLUESKY_POST ||
25
25
-
platform.type === SupportedPlatform.BLACKSKY_POST ? (
26
26
-
<Box maw={600} w={'100%'} mx={'auto'}>
27
27
-
<Suspense
28
28
-
fallback={<BlueskySembleHeaderSkeleton />}
29
29
-
key={props.url + 'bluesky header'}
30
30
-
>
31
31
-
<BlueskySembleHeader
32
32
-
url={props.url}
33
33
-
viaCardId={props.viaCardId}
34
34
-
/>
35
35
-
</Suspense>
36
36
-
</Box>
37
37
-
) : (
38
38
-
<Suspense
39
39
-
fallback={<SembleHeaderSkeleton />}
40
40
-
key={props.url + 'semble header'}
41
41
-
>
42
42
-
<SembleHeader url={props.url} viaCardId={props.viaCardId} />
43
43
-
</Suspense>
44
44
-
)}
45
45
-
<Suspense>
46
46
-
<SembleTabs url={props.url} />
48
48
+
<Suspense fallback={<SkeletonComponent />} key={props.url + 'header'}>
49
49
+
<HeaderComponent url={props.url} viaCardId={props.viaCardId} />
47
50
</Suspense>
51
51
+
52
52
+
<SembleTabs url={props.url} />
48
53
</Stack>
49
54
</Container>
50
55
</Container>