226 lines
8.3 KiB
TypeScript
226 lines
8.3 KiB
TypeScript
// components/ui/Feed.tsx
|
|
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { Fragment } from 'react';
|
|
import {
|
|
ChatBubbleLeftEllipsisIcon,
|
|
TagIcon,
|
|
UserCircleIcon,
|
|
} from '@heroicons/react/20/solid';
|
|
import clsx from 'clsx';
|
|
|
|
/* ───────── Types ───────── */
|
|
|
|
export type FeedPerson = {
|
|
name: string;
|
|
href?: string;
|
|
};
|
|
|
|
export type FeedTag = {
|
|
name: string;
|
|
href?: string;
|
|
/** z.B. 'fill-red-500' */
|
|
color?: string;
|
|
};
|
|
|
|
export type FeedItem =
|
|
| {
|
|
id: string | number;
|
|
type: 'comment';
|
|
person: FeedPerson;
|
|
imageUrl?: string;
|
|
comment: string;
|
|
date: string;
|
|
}
|
|
| {
|
|
id: string | number;
|
|
type: 'assignment';
|
|
person: FeedPerson;
|
|
assigned: FeedPerson;
|
|
date: string;
|
|
}
|
|
| {
|
|
id: string | number;
|
|
type: 'tags';
|
|
person: FeedPerson;
|
|
tags: FeedTag[];
|
|
date: string;
|
|
};
|
|
|
|
export interface FeedProps {
|
|
items: FeedItem[];
|
|
className?: string;
|
|
}
|
|
|
|
/* ───────── Helper ───────── */
|
|
|
|
function classNames(...classes: Array<string | false | null | undefined>) {
|
|
return classes.filter(Boolean).join(' ');
|
|
}
|
|
|
|
/* ───────── Component ───────── */
|
|
|
|
export default function Feed({ items, className }: FeedProps) {
|
|
if (!items.length) {
|
|
return (
|
|
<p className={clsx('text-sm text-gray-500 dark:text-gray-400', className)}>
|
|
Keine Aktivitäten vorhanden.
|
|
</p>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={clsx('flow-root', className)}>
|
|
<ul role="list" className="-mb-8">
|
|
{items.map((activityItem, idx) => (
|
|
<li key={activityItem.id}>
|
|
<div className="relative pb-8">
|
|
{idx !== items.length - 1 ? (
|
|
<span
|
|
aria-hidden="true"
|
|
className="absolute left-5 top-5 -ml-px h-full w-0.5 bg-gray-200 dark:bg-white/10"
|
|
/>
|
|
) : null}
|
|
|
|
<div className="relative flex items-start space-x-3">
|
|
{activityItem.type === 'comment' ? (
|
|
<>
|
|
<div className="relative">
|
|
{activityItem.imageUrl ? (
|
|
<img
|
|
alt=""
|
|
src={activityItem.imageUrl}
|
|
className="flex size-10 items-center justify-center rounded-full bg-gray-400 ring-8 ring-white outline -outline-offset-1 outline-black/5 dark:ring-gray-900 dark:outline-white/10"
|
|
/>
|
|
) : (
|
|
<div className="flex size-10 items-center justify-center rounded-full bg-gray-200 ring-8 ring-white dark:bg-gray-800 dark:ring-gray-900">
|
|
<ChatBubbleLeftEllipsisIcon
|
|
aria-hidden="true"
|
|
className="size-5 text-gray-400"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<span className="absolute -right-1 -bottom-0.5 rounded-tl bg-white px-0.5 py-px dark:bg-gray-900">
|
|
<ChatBubbleLeftEllipsisIcon
|
|
aria-hidden="true"
|
|
className="size-5 text-gray-400"
|
|
/>
|
|
</span>
|
|
</div>
|
|
|
|
<div className="min-w-0 flex-1">
|
|
<div>
|
|
<div className="text-sm">
|
|
<a
|
|
href={activityItem.person.href ?? '#'}
|
|
className="font-medium text-gray-900 dark:text-white"
|
|
>
|
|
{activityItem.person.name}
|
|
</a>
|
|
</div>
|
|
<p className="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
|
|
Kommentiert {activityItem.date}
|
|
</p>
|
|
</div>
|
|
<div className="mt-2 text-sm text-gray-700 dark:text-gray-200">
|
|
<p>{activityItem.comment}</p>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : activityItem.type === 'assignment' ? (
|
|
<>
|
|
<div>
|
|
<div className="relative px-1">
|
|
<div className="flex size-8 items-center justify-center rounded-full bg-gray-100 ring-8 ring-white dark:bg-gray-800 dark:ring-gray-900">
|
|
<UserCircleIcon
|
|
aria-hidden="true"
|
|
className="size-5 text-gray-500 dark:text-gray-400"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="min-w-0 flex-1 py-1.5">
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
<a
|
|
href={activityItem.person.href ?? '#'}
|
|
className="font-medium text-gray-900 dark:text-white"
|
|
>
|
|
{activityItem.person.name}
|
|
</a>{' '}
|
|
hat{' '}
|
|
<a
|
|
href={activityItem.assigned.href ?? '#'}
|
|
className="font-medium text-gray-900 dark:text-white"
|
|
>
|
|
{activityItem.assigned.name}
|
|
</a>{' '}
|
|
zugewiesen{' '}
|
|
<span className="whitespace-nowrap">
|
|
{activityItem.date}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : activityItem.type === 'tags' ? (
|
|
<>
|
|
<div>
|
|
<div className="relative px-1">
|
|
<div className="flex size-8 items-center justify-center rounded-full bg-gray-100 ring-8 ring-white dark:bg-gray-800 dark:ring-gray-900">
|
|
<TagIcon
|
|
aria-hidden="true"
|
|
className="size-5 text-gray-500 dark:text-gray-400"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="min-w-0 flex-1 py-0">
|
|
<div className="text-sm/8 text-gray-500 dark:text-gray-400">
|
|
<span className="mr-0.5">
|
|
<a
|
|
href={activityItem.person.href ?? '#'}
|
|
className="font-medium text-gray-900 dark:text-white"
|
|
>
|
|
{activityItem.person.name}
|
|
</a>{' '}
|
|
hat Tags hinzugefügt
|
|
</span>{' '}
|
|
<span className="mr-0.5">
|
|
{activityItem.tags.map((tag) => (
|
|
<Fragment key={tag.name}>
|
|
<a
|
|
href={tag.href ?? '#'}
|
|
className="inline-flex items-center gap-x-1.5 rounded-full px-2 py-1 text-xs font-medium text-gray-900 inset-ring inset-ring-gray-200 dark:bg-white/5 dark:text-gray-100 dark:inset-ring-white/10"
|
|
>
|
|
<svg
|
|
viewBox="0 0 6 6"
|
|
aria-hidden="true"
|
|
className={classNames(
|
|
tag.color ?? 'fill-gray-400',
|
|
'size-1.5',
|
|
)}
|
|
>
|
|
<circle r={3} cx={3} cy={3} />
|
|
</svg>
|
|
{tag.name}
|
|
</a>{' '}
|
|
</Fragment>
|
|
))}
|
|
</span>
|
|
<span className="whitespace-nowrap">
|
|
{activityItem.date}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|