data endpoint for entity 90008 (aka. a website)
1<script module lang="ts">
2 import type { Post } from '$lib/bluesky';
3
4 export interface OutgoingLink {
5 name: string;
6 link: string;
7 }
8
9 export interface NoteData {
10 content: string;
11 published: number;
12 hasMedia: boolean;
13 hasQuote: boolean;
14 outgoingLinks?: OutgoingLink[];
15 purposeAction?: string;
16 children?: NoteData[];
17 depth?: number;
18 }
19
20 export const flattenNotes = (note: NoteData, currentDepth: number = 0): NoteData[] => {
21 note.depth = currentDepth;
22 const flattened = [note];
23 if (note.children) {
24 note.children.forEach((child) => {
25 flattened.push(...flattenNotes(child, currentDepth + 1));
26 });
27 }
28 return flattened;
29 };
30
31 export const noteFromBskyPost = ({ record: post, uri }: Post): NoteData => {
32 return {
33 content: post.text,
34 published: new Date(post.createdAt).getTime(),
35 outgoingLinks: [{ name: 'bsky', link: uri }],
36 hasMedia:
37 (post.embed?.$type === 'app.bsky.embed.images' ||
38 post.embed?.$type === 'app.bsky.embed.video' ||
39 post.embed?.$type === 'app.bsky.embed.recordWithMedia') ??
40 false,
41 hasQuote:
42 (post.embed?.$type === 'app.bsky.embed.record' ||
43 post.embed?.$type === 'app.bsky.embed.recordWithMedia') ??
44 false
45 };
46 };
47</script>
48
49<script lang="ts">
50 import Token from './token.svelte';
51 import { renderDate, renderRelativeDate } from '$lib/dateFmt';
52
53 interface Props {
54 rootNote: NoteData;
55 isHighlighted?: boolean;
56 onlyContent?: boolean;
57 showOutgoing?: boolean;
58 mapOutgoingNames?: Record<string, string>;
59 }
60
61 let {
62 rootNote,
63 isHighlighted = false,
64 onlyContent = false,
65 showOutgoing = true,
66 mapOutgoingNames = {}
67 }: Props = $props();
68
69 const getOutgoingLink = ({ name, link }: { name: string; link: string }) => {
70 if (name.startsWith('bsky')) {
71 // Parse the atproto URI to extract DID and rkey
72 const match = link.match(/at:\/\/(did:[^/]+)\/[^/]+\/([^/]+)/);
73 if (match && match.length >= 3) {
74 // eslint-disable-next-line @typescript-eslint/no-unused-vars
75 const [_, did, rkey] = match;
76 link = `https://bsky.app/profile/${did}/post/${rkey}`;
77 }
78 if (name === 'bsky-reply') {
79 return ['reply', link];
80 } else {
81 return [name, link];
82 }
83 }
84 return [name, link];
85 };
86 // this is ASS this should be a tailwind class
87 const getTextShadowStyle = (color: string) => {
88 return `text-shadow: 0 0 1px theme(colors.ralsei.black), 0 0 5px ${color};`;
89 };
90 const outgoingLinkColors: Record<string, string> = {
91 bsky: 'rgb(0, 133, 255)',
92 reply: 'rgb(0, 133, 255)'
93 };
94</script>
95
96{#each flattenNotes(rootNote) as note}
97 <p class="m-0 max-w-[70ch] text-wrap break-words leading-tight align-middle">
98 {#if note.depth ?? 0 > 0}
99 <span class="inline-block">|{'=='.repeat(note.depth ?? 0)}</span>>
100 {/if}
101 {#if !onlyContent}
102 {#if (note.purposeAction ?? '').length > 0}
103 <Token v="({note.purposeAction!})" small={!isHighlighted} funct />
104 {/if}
105 {#if note.purposeAction !== 'reply'}
106 <Token
107 title={renderDate(note.published)}
108 v={renderRelativeDate(note.published)}
109 small={!isHighlighted}
110 />
111 {/if}
112 {/if}
113 <Token v={note.content} str />
114 {#if note.hasMedia}<Token v="-contains media-" keywd small />{/if}
115 {#if note.hasQuote}<Token v="-contains quote-" keywd small />{/if}
116 {#if showOutgoing}
117 {#each (note.outgoingLinks ?? []).map(getOutgoingLink) as [name, link]}
118 {@const color = outgoingLinkColors[name]}
119 {@const viewName = mapOutgoingNames[name] ?? name}
120 {#if viewName.length > 0}
121 <span class="text-sm"
122 ><Token v="(" punct /><a
123 class="hover:motion-safe:animate-squiggle hover:underline"
124 style="color: {color};{getTextShadowStyle(color)}"
125 href={link}>{viewName}</a
126 ><Token v=")" punct /></span
127 >
128 {/if}
129 {/each}
130 {/if}
131 </p>
132{/each}