mirror of
https://github.com/LibreChat-AI/librechat.ai.git
synced 2026-03-27 10:48:32 +07:00
* chore: upgrade eslint to v9 * chore: update package dependencies in package.json and pnpm-lock.yaml - Added `minimatch` and `serialize-javascript` dependencies with updated versions. - Upgraded `ajv` to version 6.14.0. - Removed outdated dependencies from pnpm-lock.yaml for better package management. * feat: add Stripe logos to Companies section - Introduced new company entry for Stripe in the Companies component, including both light and dark logo variants. - Updated the Companies array to display 10 logos instead of 8. - Adjusted TypeScript environment reference to point to the development types directory.
132 lines
4.7 KiB
TypeScript
132 lines
4.7 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { getPagesUnderRoute } from 'nextra/context'
|
|
import { type Page } from 'nextra'
|
|
import { SocialIcon } from 'react-social-icons'
|
|
import BlogCard from '../blog/BlogCard'
|
|
import Image from 'next/image'
|
|
import { Cards } from 'nextra/components'
|
|
import { OurAuthors, Blog } from '@/components/CardIcons'
|
|
|
|
// Known issues:
|
|
// - Mobile: social icons overflow when author has more than 4 social links
|
|
// - SocialIcon uses the generic "share" icon when the platform is unrecognized
|
|
// - "Recent Posts by" section does not support filtering by tag
|
|
// - Profile image positioning is off when the author has no bio text
|
|
|
|
interface AuthorMetadata {
|
|
authorid: string
|
|
subtitle: string
|
|
name: string
|
|
bio: string
|
|
ogImage: string
|
|
socials?: Record<string, string | undefined> // Dynamically match social media platforms
|
|
date: string | number | Date
|
|
}
|
|
|
|
interface AuthorProfileProps {
|
|
authorId: string
|
|
}
|
|
|
|
const AuthorProfile: React.FC<AuthorProfileProps> = ({ authorId }) => {
|
|
const authors = getPagesUnderRoute('/authors') as Array<Page & { frontMatter: AuthorMetadata }>
|
|
const author = authors.find((a) => a.frontMatter.authorid === authorId)?.frontMatter
|
|
const blogPosts = getPagesUnderRoute('/blog') as Array<Page & { frontMatter: AuthorMetadata }>
|
|
|
|
// Filter posts by the current authorId
|
|
const authorPosts = blogPosts.filter((post) => post.frontMatter.authorid === authorId)
|
|
const sortedAuthorPosts = authorPosts.sort(
|
|
(a, b) => new Date(b.frontMatter.date).getTime() - new Date(a.frontMatter.date).getTime(),
|
|
)
|
|
|
|
// State to track whether the component is rendered on the client side
|
|
const [isClient, setIsClient] = useState(false)
|
|
|
|
useEffect(() => {
|
|
setIsClient(true)
|
|
}, [])
|
|
|
|
if (!author) {
|
|
return <div>Author not found!</div>
|
|
}
|
|
|
|
const socialsEntries = Object.entries(author.socials ?? {}).filter(([, value]) => !!value)
|
|
|
|
return (
|
|
<>
|
|
<section className="max-w-4xl mx-auto flex flex-col md:flex-row gap-8 mt-12 mb-24 md:mb-32">
|
|
<div>
|
|
<h1 className="font-extrabold text-3xl lg:text-5xl tracking-tight mb-2">{author.name}</h1>
|
|
<p
|
|
className="md:text-lg mb-6 md:mb-10 font-medium"
|
|
style={{ fontSize: '1.3rem', fontWeight: 'bold' }}
|
|
>
|
|
{author.subtitle}
|
|
</p>
|
|
{author.bio && <p className="md:text-lg text-base-content/80">{author.bio}</p>}
|
|
</div>
|
|
|
|
<div className="max-md:order-first flex md:flex-col gap-4 shrink-0">
|
|
<Image
|
|
width={512}
|
|
height={512}
|
|
src={author.ogImage}
|
|
alt={author.name}
|
|
className="rounded-box size-[12rem] md:size-[16rem] rounded-square"
|
|
style={{ borderRadius: '20px', objectFit: 'cover' }}
|
|
/>
|
|
|
|
<div className="flex flex-wrap gap-4 max-w-[4rem] md:max-w-[16rem]">
|
|
{isClient &&
|
|
socialsEntries.map(([key, value]) => (
|
|
<a
|
|
key={key}
|
|
href={value}
|
|
className="btn btn-square relative overflow-hidden"
|
|
aria-label={`Visit ${author.name}'s ${key}`}
|
|
title={`See ${author.name}'s ${key}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
style={{ transition: 'transform 0.3s ease' }} // Add transition here
|
|
>
|
|
<SocialIcon
|
|
url={value}
|
|
className="absolute inset-0 size-full scale-100 transition-transform opacity-100 hover:scale-90"
|
|
bgColor="#9B9B9B80"
|
|
fgColor="background"
|
|
// fallback={{ path: 'M32 2 A30 30 0 0 1 62 32 A30 30 0 0 1 32 62 A30 30 0 0 1 2 32 A30 30 0 0 1 32 2 Z' }}
|
|
/>
|
|
</a>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section className="max-w-4xl mx-auto mt-8">
|
|
<h2 className="font-bold text-2xl mb-6 text-center">Recent Posts by {author.name}</h2>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-7">
|
|
{sortedAuthorPosts.map((post) => (
|
|
<BlogCard
|
|
key={post.route}
|
|
page={post}
|
|
handleTagClick={() => {}}
|
|
selectedTags={undefined}
|
|
/>
|
|
))}
|
|
</div>
|
|
<div style={{ marginTop: '75px' }} />
|
|
<div>
|
|
<Cards num={3}>
|
|
<Cards.Card title="Blog" href="/blog" icon={<Blog />} image>
|
|
{null}
|
|
</Cards.Card>
|
|
<Cards.Card title="Our Authors" href="/authors" icon={<OurAuthors />} image>
|
|
{null}
|
|
</Cards.Card>
|
|
</Cards>
|
|
</div>
|
|
</section>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default AuthorProfile
|