Skip to content
Merged
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
45 changes: 45 additions & 0 deletions apps/dashboard/src/components/details/detail-activity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useState } from "react";

export function DetailActivityHeader({
title,
count,
}: {
title: string;
count?: number;
}) {
return (
<div className="flex items-center justify-between gap-2 rounded-lg bg-surface-1 px-4 py-2.5">
<h2 className="text-xs font-medium">{title}</h2>
{count != null && (
<span className="text-xs tabular-nums text-muted-foreground">
{count}
</span>
)}
</div>
);
}

export function DetailCommentBox() {
const [value, setValue] = useState("");

return (
<div className="flex flex-col gap-2 rounded-lg border bg-surface-0 p-3">
<textarea
value={value}
onChange={(event) => setValue(event.target.value)}
placeholder="Leave a comment..."
rows={3}
className="w-full resize-none bg-transparent text-sm outline-none placeholder:text-muted-foreground"
/>
<div className="flex justify-end">
<button
type="button"
disabled={!value.trim()}
className="rounded-lg bg-foreground px-3 py-1.5 text-xs font-medium text-background transition-opacity disabled:opacity-40"
>
Send
</button>
</div>
</div>
);
}
107 changes: 107 additions & 0 deletions apps/dashboard/src/components/details/detail-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Skeleton } from "@diffkit/ui/components/skeleton";
import { cn } from "@diffkit/ui/lib/utils";
import { Link } from "@tanstack/react-router";

type DetailHeaderIcon = React.ComponentType<{
size?: number;
strokeWidth?: number;
className?: string;
}>;

export function DetailPageLayout({
main,
sidebar,
}: {
main: React.ReactNode;
sidebar: React.ReactNode;
}) {
return (
<div className="h-full overflow-auto">
<div className="mx-auto grid max-w-7xl gap-16 px-6 py-10 xl:grid-cols-[minmax(0,1fr)_minmax(16rem,20rem)]">
<div className="flex min-w-0 flex-col gap-8">{main}</div>
{sidebar}
</div>
</div>
);
}

export function DetailPageTitle({
collectionHref,
collectionLabel,
owner,
repo,
number,
icon: Icon,
iconClassName,
title,
subtitle,
}: {
collectionHref: string;
collectionLabel: string;
owner: string;
repo: string;
number: number;
icon: DetailHeaderIcon;
iconClassName?: string;
title: string;
subtitle: React.ReactNode;
}) {
return (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Link
to={collectionHref}
className="transition-colors hover:text-foreground"
>
{collectionLabel}
</Link>
<span>/</span>
<span>
{owner}/{repo}
</span>
<span>/</span>
<span>#{number}</span>
</div>

<div className="flex items-start gap-3">
<div className={cn("mt-1 shrink-0", iconClassName)}>
<Icon size={20} strokeWidth={2} />
</div>
<div className="flex min-w-0 flex-col gap-2">
<h1 className="text-xl font-semibold tracking-tight">{title}</h1>
{subtitle}
</div>
</div>
</div>
);
}

export function DetailPageSkeletonLayout({
main,
sidebarSectionCount = 4,
}: {
main: React.ReactNode;
sidebarSectionCount?: number;
}) {
return (
<div className="h-full overflow-auto">
<div className="mx-auto grid max-w-7xl gap-16 px-6 py-10 xl:grid-cols-[minmax(0,1fr)_minmax(16rem,20rem)]">
<div className="flex min-w-0 flex-col gap-8">{main}</div>
<aside className="flex h-fit flex-col gap-6 xl:sticky xl:top-10">
{Array.from(
{ length: sidebarSectionCount },
(_, index) => `sidebar-skeleton-${index}`,
).map((key) => (
<div key={key} className="flex flex-col gap-2.5">
<Skeleton className="h-4 w-24 rounded-md" />
<div className="flex flex-col gap-2">
<Skeleton className="h-4 w-full rounded-md" />
<Skeleton className="h-4 w-[85%] rounded-md" />
</div>
</div>
))}
</aside>
</div>
</div>
);
}
83 changes: 83 additions & 0 deletions apps/dashboard/src/components/details/detail-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@diffkit/ui/components/tooltip";

type DetailRowIcon = React.ComponentType<{
size?: number;
strokeWidth?: number;
className?: string;
}>;

export function DetailSidebar({ children }: { children: React.ReactNode }) {
return (
<aside className="flex h-fit flex-col gap-6 xl:sticky xl:top-10">
{children}
</aside>
);
}

export function DetailSidebarSection({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<div className="flex flex-col gap-2.5">
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{title}
</h3>
{children}
</div>
);
}

export function DetailSidebarRow({
icon: Icon,
label,
children,
}: {
icon?: DetailRowIcon;
label: string;
children: React.ReactNode;
}) {
return (
<div className="flex items-center justify-between gap-4">
<span className="flex items-center gap-1.5 text-muted-foreground">
{Icon ? <Icon size={13} strokeWidth={2} /> : null}
{label}
</span>
<span className="text-foreground">{children}</span>
</div>
);
}

export function DetailParticipantAvatars({
actors,
}: {
actors: Array<{
login: string;
avatarUrl: string;
}>;
}) {
return (
<div className="group/participants flex items-center">
{actors.map((actor, index) => (
<Tooltip key={actor.login}>
<TooltipTrigger asChild>
<img
src={actor.avatarUrl}
alt={actor.login}
className="size-6 rounded-full border-2 border-card transition-[margin] duration-200 group-hover/participants:ml-0"
style={index > 0 ? { marginLeft: -6 } : undefined}
/>
</TooltipTrigger>
<TooltipContent>{actor.login}</TooltipContent>
</Tooltip>
))}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Markdown } from "@diffkit/ui/components/markdown";
import { cn } from "@diffkit/ui/lib/utils";
import {
DetailActivityHeader,
DetailCommentBox,
} from "#/components/details/detail-activity";
import { formatRelativeTime } from "#/lib/format-relative-time";
import type { IssueComment } from "#/lib/github.types";

export function IssueDetailActivitySection({
comments,
isFetching,
}: {
comments?: IssueComment[];
isFetching: boolean;
}) {
return (
<div className="flex flex-col">
<DetailActivityHeader title="Activity" count={comments?.length} />

{isFetching && !comments && (
<div className="flex items-center justify-center py-8">
<svg
className="size-4 animate-spin text-muted-foreground"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<circle
cx="8"
cy="8"
r="6.5"
stroke="currentColor"
strokeWidth="2"
opacity="0.25"
/>
<path
d="M14.5 8a6.5 6.5 0 0 0-6.5-6.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
</div>
)}

{comments && comments.length === 0 && (
<p className="py-4 text-sm text-muted-foreground">No comments yet.</p>
)}

{comments && comments.length > 0 && (
<div className="relative flex flex-col pl-8 before:absolute before:left-4 before:top-0 before:h-full before:w-px before:bg-[linear-gradient(to_bottom,var(--color-border)_80%,transparent)]">
{comments.map((comment, index) => (
<div
key={comment.id}
className={cn("flex flex-col gap-1 py-4", index === 0 && "pt-5")}
>
<div className="flex items-center gap-1.5">
{comment.author ? (
<img
src={comment.author.avatarUrl}
alt={comment.author.login}
className="size-4 rounded-full border border-border"
/>
) : (
<div className="size-4 rounded-full bg-surface-2" />
)}
<span className="text-xs font-medium">
{comment.author?.login ?? "Unknown"}
</span>
<span className="text-xs text-muted-foreground">
{formatRelativeTime(comment.createdAt)}
</span>
</div>
<Markdown className="text-muted-foreground">
{comment.body}
</Markdown>
</div>
))}
</div>
)}

<div className="mt-6">
<DetailCommentBox />
</div>
</div>
);
}
Loading
Loading