import {
BookMarked,
CircleDot,
FolderCode,
GalleryVertical,
GitPullRequest,
LoaderCircle,
UserRound,
TrendingUp,
ShieldCheck,
Shield,
ShieldPlus,
Megaphone,
} from 'lucide-solid';
import clsx from 'clsx';
import { A } from '@solidjs/router';
import { createQuery } from '@tanstack/solid-query';
import { For, Show, createEffect, createMemo, createSignal, onCleanup, type Component, type JSX } from 'solid-js';
import { Avatar, ErrorState, LoadingState, StateBadge } from '../components/common';
import { RepoCard } from '../components/repo';
import { listFollowRecords } from '../lib/api/graph';
import { resolveActor } from '../lib/api/identity';
import { listRepoRecords, type RepoRecord } from '../lib/api/repos';
import { useAuth } from '../lib/auth';
import { useLiveEvents, type LiveEvent } from '../lib/live-events';
import { useIssuePreloader, useRepoPreloader } from '../lib/preloading';
import { formatRelativeTime, getErrorMessage, loadRecentRepos, loadRecentIssuesPulls } from '../lib/repo-utils';
const SAMPLE_REPOS: Array<{
owner: string;
slug: string;
description?: string;
language?: string;
stars?: number;
forks?: number;
openIssues?: number;
openPRs?: number;
}> = [
{ owner: 'nekomimi.pet', slug: 'wisp.place-monorepo', description: 'Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.', language: 'TypeScript', stars: 94, openIssues: 39, openPRs: 23 },
{ owner: 'mary.my.id', slug: 'atcute', description: 'a collection of lightweight TypeScript packages for AT Protocol, the protocol powering Bluesky', language: 'TypeScript', stars: 101, openIssues: 8 },
{ owner: 'pds.ls', slug: 'pdsls', description: 'atmosphere explorer', language: 'TypeScript', stars: 491, openIssues: 20, openPRs: 21 },
];
const FALLBACK_ENTRIES = [
{
title: 'tail CI logs over SSH',
link: 'https://blog.tangled.org/ssh',
summary: 'never leave your terminal to check on CI!',
updated: '2026-05-28T00:00:00Z',
author: 'Akshay'
},
{
title: 'combat LLM spam by building a web of trust',
link: 'https://blog.tangled.org/vouching',
summary: 'vouching on tangled!',
updated: '2026-05-01T00:00:00Z',
author: 'Akshay'
},
{
title: 'we need a federation of forges',
link: 'https://blog.tangled.org/federation',
summary: 'git is decentralized, but what of the rest?',
updated: '2026-04-29T00:00:00Z',
author: 'Akshay'
},
{
title: 'newsletter 01 — hello!',
link: 'https://blog.tangled.org/newsletter-01',
summary: 'kicking off our newsletter!',
updated: '2026-04-16T00:00:00Z',
author: 'Anirudh'
},
{
title: 'announcing our €3,8M seed round',
link: 'https://blog.tangled.org/seed',
summary: "and more on what's next",
updated: '2026-03-02T00:00:00Z',
author: 'Anirudh'
}
];
const fetchBlogFeed = async (): Promise<any[]> => {
try {
const response = await fetch('https://blog.tangled.org/feed.xml');
if (!response.ok) throw new Error('Failed to fetch');
const text = await response.text();
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(text, 'text/xml');
const entries = xmlDoc.getElementsByTagName('entry');
const posts = [];
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const title = entry.getElementsByTagName('title')[0]?.textContent || '';
const updated = entry.getElementsByTagName('updated')[0]?.textContent || '';
const summary = entry.getElementsByTagName('summary')[0]?.textContent || '';
const authorName = entry.getElementsByTagName('author')[0]?.getElementsByTagName('name')[0]?.textContent || '';
let href = '';
const links = entry.getElementsByTagName('link');
for (let j = 0; j < links.length; j++) {
const link = links[j];
if (link.getAttribute('rel') === 'alternate' || !link.getAttribute('rel')) {
href = link.getAttribute('href') || '';
break;
}
}
posts.push({
title,
link: href || 'https://blog.tangled.org',
summary,
updated,
author: authorName,
});
}
return posts;
} catch (err) {
console.warn('CORS or network error fetching blog feed, using fallback', err);
return FALLBACK_ENTRIES;
}
};
const formatBlogDate = (dateStr: string) => {
try {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
} catch {
return dateStr;
}
};
const ANNOUNCEMENT_COLORS = [
'untangled-home-promo-card-indigo',
'untangled-home-promo-card-green',
'untangled-home-promo-card-purple',
'untangled-home-promo-card-blue',
'untangled-home-promo-card-amber'
];
const hashStringToIndex = (str: string, count: number): number => {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
return Math.abs(hash) % count;
};
const AnnouncementCard: Component<{
title: string;
link: string;
summary: string;
updated: string;
author: string;
cardClass: string;
}> = (props) => {
const formattedDate = () => formatBlogDate(props.updated);
return (
<div class={clsx('relative overflow-hidden border rounded-sm p-6', props.cardClass)}>
<div class="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_left,rgba(255,255,255,0.25),transparent_70%)]"></div>
<div class="relative z-10 flex flex-col gap-1 text-inherit">
<div class="flex items-center justify-between text-xs opacity-75 mb-1 text-inherit">
<span>{props.author}</span>
<span>{formattedDate()}</span>
</div>
<h4 class="font-bold mb-1 text-base leading-snug text-inherit">
{props.title}
</h4>
<p class="text-sm margin-0 opacity-90 text-inherit">
{props.summary}
</p>
<div class="mt-3 flex items-center justify-between">
<a
class="no-underline inline-flex items-center gap-1 text-sm font-semibold hover:underline text-inherit"
href={props.link}
target="_blank"
rel="noopener noreferrer"
>
Read more →
</a>
</div>
</div>
</div>
);
};
const HOME_ACTIVITY_SOURCES = [
'sh.tangled.repo.issue:repoDid',
'sh.tangled.repo.issue:repo',
'sh.tangled.repo.pull:target.repo',
];
type HomeRepoItem = {
owner: string;
slug: string;
title: string;
href: string;
rkey: string;
repoDid?: string;
description?: string;
createdAt?: string;
};
type HomeActivityKind = 'issue' | 'pull' | 'repo';
type HomeActivityItem = {
id: string;
kind: HomeActivityKind;
title: string;
repoLabel: string;
href: string;
uri?: string;
at?: string;
body?: string;
actorDid?: string;
actorHandle?: string;
};
type FollowingHomeData = {
actors: Array<{
did: string;
handle: string;
repoCount: number;
}>;
activity: HomeActivityItem[];
};
const repoSlug = (repo: RepoRecord): string => repo.value.name?.trim() || repo.rkey;
const repoTimestamp = (repo: HomeRepoItem): number => {
const timestamp = repo.createdAt ? new Date(repo.createdAt).getTime() : 0;
return Number.isFinite(timestamp) ? timestamp : 0;
};
const activityTimestamp = (item: HomeActivityItem): number => {
const timestamp = item.at ? new Date(item.at).getTime() : 0;
return Number.isFinite(timestamp) ? timestamp : 0;
};
const homeRepoItem = (owner: string, repo: RepoRecord): HomeRepoItem => {
const slug = repoSlug(repo);
return {
owner,
slug,
title: `${owner}/${slug}`,
href: `/${owner}/${slug}`,
rkey: repo.rkey,
repoDid: repo.value.repoDid,
description: repo.value.description,
createdAt: repo.value.createdAt,
};
};
const activityLabelForEvent = (event: LiveEvent): Pick<HomeActivityItem, 'kind' | 'title'> => {
if (event.source.includes('.pull')) {
return { kind: 'pull', title: 'new pull request' };
}
return { kind: 'issue', title: 'new issue' };
};
const eventActivityHref = (repo: HomeRepoItem, event: LiveEvent): string => {
const collection = event.source.includes('.pull') ? 'pulls' : 'issues';
return `${repo.href}/${collection}/${encodeURIComponent(event.sourceRecord)}`;
};
const fetchFollowingHomeData = async (viewerDid: string): Promise<FollowingHomeData> => {
const follows = await listFollowRecords(viewerDid);
const resolved = await Promise.all(
follows.slice(0, 8).map(async (follow) => {
try {
const actor = await resolveActor(follow.value.subject);
const repos = await listRepoRecords(actor).catch(() => []);
return {
follow,
actor,
repos,
};
} catch {
return null;
}
}),
);
const actors = resolved
.filter((item): item is NonNullable<typeof item> => Boolean(item))
.map((item) => ({
did: item.actor.did,
handle: item.actor.handle,
repoCount: item.repos.length,
}));
const activity = resolved
.filter((item): item is NonNullable<typeof item> => Boolean(item))
.flatMap((item) =>
item.repos.map((repo) => {
const repoItem = homeRepoItem(item.actor.handle, repo);
return {
id: `following-repo:${repo.uri}`,
kind: 'repo' as const,
title: 'repository published',
repoLabel: repoItem.title,
href: repoItem.href,
at: repoItem.createdAt,
body: repoItem.description,
actorDid: item.actor.did,
actorHandle: item.actor.handle,
};
}),
)
.sort((left, right) => activityTimestamp(right) - activityTimestamp(left))
.slice(0, 8);
return { actors, activity };
};
export const HomePage: Component = () => {
const auth = useAuth();
const live = useLiveEvents();
const issuePreloader = useIssuePreloader();
const recentRepos = createMemo(loadRecentRepos);
const recentIssuesPulls = createMemo(loadRecentIssuesPulls);
const ownReposQuery = createQuery(() => ({
queryKey: ['own-repos', auth.currentDid()],
enabled: Boolean(auth.currentDid()),
queryFn: async () => {
const actor = await resolveActor(auth.currentDid()!);
const records = await listRepoRecords(actor);
return { actor, records };
},
}));
const followingQuery = createQuery(() => ({
queryKey: ['home-following', auth.currentDid()],
enabled: Boolean(auth.currentDid()),
queryFn: async () => fetchFollowingHomeData(auth.currentDid()!),
}));
const [vouchedHandles, setVouchedHandles] = createSignal<string[]>([]);
const vouchSuggestionsQuery = createQuery(() => ({
queryKey: ['vouch-suggestions'],
queryFn: async () => {
const handles = ['nekomimi.pet', 'mary.my.id', 'juliet.paris'];
const resolved = await Promise.all(
handles.map(async (handle) => {
try {
const actor = await resolveActor(handle);
return {
did: actor.did,
handle: actor.handle,
};
} catch {
return {
did: `did:placeholder:${handle}`,
handle,
};
}
})
);
return resolved;
}
}));
const blogQuery = createQuery(() => ({
queryKey: ['blog-feed'],
queryFn: fetchBlogFeed,
}));
const ownRepoItems = createMemo(() => {
const data = ownReposQuery.data;
if (!data) return [];
return data.records
.map((repo) => homeRepoItem(data.actor.handle, repo))
.sort((left, right) => repoTimestamp(right) - repoTimestamp(left));
});
const currentUserHandle = () => ownReposQuery.data?.actor.handle || '';
const currentUserDid = () => auth.currentDid() || '';
const repoByDid = createMemo(() => {
const repos = new Map<string, HomeRepoItem>();
for (const repo of ownRepoItems()) {
if (repo.repoDid) repos.set(repo.repoDid, repo);
}
return repos;
});
createEffect(() => {
if (!auth.currentDid()) {
live.clear();
return;
}
if (ownReposQuery.isLoading) {
return;
}
const subjects = ownRepoItems()
.map((repo) => repo.repoDid)
.filter((repoDid): repoDid is string => Boolean(repoDid));
if (subjects.length === 0) {
live.clear();
return;
}
live.subscribe({
subjects,
sources: HOME_ACTIVITY_SOURCES,
});
onCleanup(() => {
live.clear();
});
});
const activityItems = createMemo<HomeActivityItem[]>(() => {
const repos = repoByDid();
const liveItems = live.events()
.map((event): HomeActivityItem | null => {
const repo = repos.get(event.subject);
if (!repo) return null;
const label = activityLabelForEvent(event);
return {
id: `live:${event.sourceRecord}:${event.receivedAt}`,
...label,
repoLabel: repo.title,
href: eventActivityHref(repo, event),
uri: event.sourceRecord,
at: event.receivedAt,
actorDid: currentUserDid(),
actorHandle: currentUserHandle(),
};
})
.filter((item): item is HomeActivityItem => Boolean(item));
const repoItems: HomeActivityItem[] = ownRepoItems().map((repo) => ({
id: `repo:${repo.owner}/${repo.rkey}`,
kind: 'repo' as const,
title: 'repository published',
repoLabel: repo.title,
href: repo.href,
at: repo.createdAt,
body: repo.description,
actorDid: currentUserDid(),
actorHandle: currentUserHandle(),
}));
return [...liveItems, ...repoItems]
.sort((left, right) => activityTimestamp(right) - activityTimestamp(left))
.slice(0, 8);
});
return (
<div class="untangled-home-page">
<section class="untangled-home-workspace">
<main class="untangled-home-feed">
<div class="untangled-home-feed-header">
<h2 class="untangled-home-feed-title">
<GalleryVertical class="size-5" />
activity
</h2>
</div>
<Show
when={auth.currentDid()}
fallback={<HomeFeedEmpty title="Sign in for repo activity" body="Issues, pull requests, and repos from your account will appear here." />}
>
<Show
when={!ownReposQuery.isLoading}
fallback={<HomeFeedLoading />}
>
<Show
when={!ownReposQuery.error}
fallback={<ErrorState message={getErrorMessage(ownReposQuery.error)} />}
>
<Show
when={activityItems().length > 0}
fallback={<HomeFeedEmpty title="No repo activity yet" body="New repo, issue, and pull request activity will land here." />}
>
<div class="untangled-home-activity-list">
<For each={activityItems()}>
{(item, index) => <ActivityRow item={item} index={index()} />}
</For>
</div>
</Show>
</Show>
</Show>
</Show>
</main>
<aside class="untangled-home-side untangled-home-side-right flex flex-col gap-6">
{/* Recent Repos */}
<div class="flex flex-col gap-3 w-full">
<HomeSectionHeader icon={<BookMarked class="size-5 text-gray-500" />} title="recent" />
<Show
when={recentRepos().length > 0}
fallback={<HomeEmpty label="No recent repos." />}
>
<div class="flex flex-col gap-3">
<For each={recentRepos().slice(0, 5)}>
{(repo) => (
<RepoCard
href={`/${repo.owner}/${repo.slug}`}
owner={repo.owner}
name={repo.slug}
description={repo.description}
isFork={repo.isFork}
language={repo.language}
stars={repo.stars}
openIssues={repo.openIssues}
openPRs={repo.openPRs}
compact
/>
)}
</For>
</div>
</Show>
</div>
{/* Recent Issues / PRs */}
<div class="flex flex-col gap-3 w-full">
<HomeSectionHeader icon={<CircleDot class="size-5" />} title="recent issues / prs" />
<Show
when={recentIssuesPulls().length > 0}
fallback={<HomeEmpty label="No recent issues or pull requests." />}
>
<div class="untangled-home-compact-list">
<For each={recentIssuesPulls().slice(0, 5)}>
{(item) => {
const preloadProps = issuePreloader(item.repoLabel, item.kind, item.uri, item.number);
return (
<A href={item.href} class="untangled-home-repo-row" {...preloadProps}>
<div class="text-sm font-semibold text-black dark:text-white truncate">
{item.title}{' '}
<span class="text-gray-500 dark:text-gray-400 font-normal">
#{item.uri.split('/').pop()}
</span>
</div>
<div class="mt-2 flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 min-w-0">
<StateBadge state={item.state} kind={item.kind} />
<span class="truncate min-w-0">
{item.author} · {item.repoLabel}
</span>
</div>
</A>
);
}}
</For>
</div>
</Show>
</div>
</aside>
</section>
<section class="untangled-home-below">
<div class="untangled-home-lower-grid">
{/* Left sidebar announcements */}
<aside class="untangled-home-side untangled-home-promos-column">
{/* Announcements Section */}
<div class="flex flex-col gap-3 w-full">
<HomeSectionHeader icon={<Megaphone class="size-5" />} title="announcements" />
<div class="untangled-home-announcements-grid">
<Show when={!blogQuery.isLoading} fallback={<LoadingState label="Loading announcements..." />}>
<For each={blogQuery.data?.slice(0, 5) ?? []}>
{(post) => {
const colorIndex = hashStringToIndex(post.title, ANNOUNCEMENT_COLORS.length);
const cardClass = ANNOUNCEMENT_COLORS[colorIndex];
return (
<AnnouncementCard
title={post.title}
link={post.link}
summary={post.summary}
updated={post.updated}
author={post.author}
cardClass={cardClass}
/>
);
}}
</For>
</Show>
</div>
</div>
</aside>
<section class="untangled-home-wide-section untangled-home-lower-left">
<HomeSectionHeader icon={<UserRound class="size-5" />} title="timeline" />
<Show
when={auth.currentDid()}
fallback={<HomeEmpty label="Sign in to see followed repo activity." />}
>
<Show
when={!followingQuery.isLoading}
fallback={<LoadingState label="Loading followed repos..." />}
>
<Show
when={!followingQuery.error}
fallback={<ErrorState message={getErrorMessage(followingQuery.error)} />}
>
<Show
when={(followingQuery.data?.activity.length ?? 0) > 0}
fallback={<HomeEmpty label="No followed repo activity yet." />}
>
<div class="untangled-home-activity-list">
<For each={followingQuery.data?.activity ?? []}>
{(item, index) => <ActivityRow item={item} index={index()} />}
</For>
</div>
</Show>
</Show>
</Show>
</Show>
</section>
<aside class="untangled-home-side flex flex-col gap-6 untangled-home-lower-right">
<div class="flex flex-col gap-6 w-full untangled-home-sticky-wrapper">
<section class="untangled-home-wide-section">
<HomeSectionHeader icon={<TrendingUp class="size-5" />} title="trending" />
<div class="flex flex-col gap-3">
<For each={SAMPLE_REPOS}>
{(repo) => (
<RepoCard
href={`/${repo.owner}/${repo.slug}`}
owner={repo.owner}
name={repo.slug}
description={repo.description}
language={repo.language}
stars={repo.stars}
forks={repo.forks}
openIssues={repo.openIssues}
compact
/>
)}
</For>
</div>
</section>
{/* Suggested (Vouches) Section */}
<div class="flex flex-col gap-3 w-full">
<header class="untangled-home-section-header flex justify-between items-center w-full">
<div class="flex items-center gap-3">
<ShieldPlus class="size-5" />
<h2>suggested</h2>
</div>
<Show when={auth.currentDid()}>
<A href={`/${ownReposQuery.data?.actor.handle || ownReposQuery.data?.actor.did || ''}?tab=vouches`} class="text-sm font-normal hover:underline text-gray-500 dark:text-gray-400">
view all →
</A>
</Show>
</header>
<div class="flex flex-col gap-4 bg-white dark:bg-gray-800 p-4 border border-gray-200 dark:border-gray-700 shadow-sm rounded-sm">
<Show when={!vouchSuggestionsQuery.isLoading} fallback={<LoadingState label="Loading suggestions..." />}>
<For each={vouchSuggestionsQuery.data ?? []}>
{(suggestion) => {
const isVouched = () => vouchedHandles().includes(suggestion.handle);
const toggleVouch = () => {
if (!auth.currentDid()) {
alert('Please sign in to vouch for users.');
return;
}
if (isVouched()) {
setVouchedHandles(prev => prev.filter(h => h !== suggestion.handle));
} else {
setVouchedHandles(prev => [...prev, suggestion.handle]);
}
};
return (
<div class="flex items-center gap-3">
<A href={`/${suggestion.handle}`}>
<Avatar did={suggestion.did} size="size-10" />
</A>
<div class="flex flex-col min-w-0 flex-1">
<A href={`/${suggestion.handle}`} class="text-sm font-medium dark:text-white hover:underline truncate">
{suggestion.handle}
</A>
<span class="text-xs text-gray-500 dark:text-gray-400 truncate">
</span>
</div>
<div class="shrink-0">
<button
type="button"
onClick={toggleVouch}
class={isVouched() ? 'btn-create text-xs px-2.5 py-1.5' : 'btn text-xs px-2.5 py-1.5'}
>
<Show when={isVouched()} fallback={<><Shield class="size-3.5 mr-1 inline" /> vouch</>}>
<ShieldCheck class="size-3.5 mr-1 inline" /> vouched
</Show>
</button>
</div>
</div>
);
}}
</For>
</Show>
</div>
</div>
</div>
</aside>
</div>
</section>
</div>
);
};
const HomeSectionHeader: Component<{ icon: JSX.Element; title: string }> = (props) => (
<header class="untangled-home-section-header">
{props.icon}
<h2>{props.title}</h2>
</header>
);
const HomeEmpty: Component<{ label: string }> = (props) => (
<div class="untangled-home-empty">{props.label}</div>
);
const HomeFeedEmpty: Component<{ title: string; body: string }> = (props) => (
<div class="untangled-home-feed-empty">
<FolderCode class="size-8" />
<h3>{props.title}</h3>
<p>{props.body}</p>
</div>
);
const HomeFeedLoading: Component = () => (
<div class="untangled-home-feed-empty">
<LoaderCircle class="size-8 animate-spin" />
<h3>Loading activity</h3>
<p>Checking your repos.</p>
</div>
);
const ActivityRow: Component<{ item: HomeActivityItem; index: number }> = (props) => {
const repoUrl = () => `/${props.item.repoLabel}`;
const actorUrl = () => (props.item.actorHandle ? `/${props.item.actorHandle}` : '#');
const preloadRepo = useRepoPreloader();
const issuePreloader = useIssuePreloader();
const actionText = () => {
if (props.item.kind === 'repo') return 'published repo';
if (props.item.kind === 'issue') return 'created issue in';
if (props.item.kind === 'pull') return 'opened pull request in';
return 'updated';
};
const preloadProps = () => {
if (props.item.kind === 'issue' || props.item.kind === 'pull') {
if (props.item.uri) {
return issuePreloader(props.item.repoLabel, props.item.kind, props.item.uri);
}
}
return {};
};
const preloadRepoProps = () => {
const [owner, slug] = props.item.repoLabel.split('/');
return preloadRepo(owner, slug);
};
return (
<div class="relative">
<Show when={props.index > 0}>
<div class="absolute left-8 -top-4 w-px h-4 bg-gray-300 dark:bg-gray-600" />
</Show>
<div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-sm">
{/* Header row */}
<div class="pl-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex items-center gap-2 text-sm min-w-0 overflow-hidden">
<Show when={props.item.actorDid}>
<Avatar did={props.item.actorDid!} size="size-5" />
</Show>
<A
href={actorUrl()}
class="font-bold no-underline hover:underline text-black dark:text-white truncate shrink-0 max-w-[8rem] sm:max-w-none"
>
{props.item.actorHandle || 'user'}
</A>
<span class="whitespace-nowrap shrink-0">{actionText()}</span>
<A
href={repoUrl()}
class="no-underline hover:underline font-bold text-black dark:text-white truncate min-w-0"
{...preloadRepoProps()}
>
{props.item.repoLabel}
</A>
<span class="text-gray-400 dark:text-gray-500 text-xs ml-auto pr-6 shrink-0 whitespace-nowrap">
{formatRelativeTime(props.item.at)}
</span>
</div>
{/* Content row */}
<Show
when={props.item.kind === 'repo'}
fallback={
<div class="py-4 px-6 flex flex-col gap-1 bg-white dark:bg-gray-800">
<div class="font-medium text-black dark:text-white flex items-center">
<Show
when={props.item.kind === 'pull'}
fallback={<CircleDot class="w-4 h-4 mr-1.5 shrink-0 text-green-600" />}
>
<GitPullRequest class="w-4 h-4 mr-1.5 shrink-0 text-purple-600" />
</Show>
<A
href={props.item.href}
class="no-underline hover:underline font-medium text-black dark:text-white text-base"
{...preloadProps()}
>
{props.item.title}
</A>
</div>
<Show when={props.item.body}>
<div class="text-gray-600 dark:text-gray-300 text-sm line-clamp-2 mt-1">{props.item.body}</div>
</Show>
</div>
}
>
<RepoCard
owner={props.item.repoLabel.split('/')[0]}
name={props.item.repoLabel.split('/')[1]}
description={props.item.body}
href={props.item.href}
noBorder
/>
</Show>
</div>
</div>
);
};