mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-11 20:00:23 +01:00
Add ProfileSidebar component for user profile display and update route to use it
This commit is contained in:
97
packages/web/src/components/ProfileSidebar.tsx
Normal file
97
packages/web/src/components/ProfileSidebar.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { Link } from "@tanstack/react-router"
|
||||||
|
import { ExternalLink, MapPin, Calendar, Users } from "lucide-react"
|
||||||
|
|
||||||
|
interface ProfileSidebarProps {
|
||||||
|
user: {
|
||||||
|
id: string
|
||||||
|
name: string | null
|
||||||
|
username: string
|
||||||
|
image: string | null
|
||||||
|
bio?: string | null
|
||||||
|
website?: string | null
|
||||||
|
location?: string | null
|
||||||
|
joinedAt?: string | null
|
||||||
|
}
|
||||||
|
isLive?: boolean
|
||||||
|
viewerCount?: number
|
||||||
|
children?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProfileSidebar({ user, isLive, viewerCount, children }: ProfileSidebarProps) {
|
||||||
|
const displayName = user.name || user.username
|
||||||
|
const avatarUrl = user.image || `https://api.dicebear.com/7.x/initials/svg?seed=${user.username}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col bg-black border-l border-white/10">
|
||||||
|
{/* Profile Header */}
|
||||||
|
<div className="p-4 border-b border-white/10">
|
||||||
|
{/* Avatar and Live Badge */}
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src={avatarUrl}
|
||||||
|
alt={displayName}
|
||||||
|
className="w-16 h-16 rounded-full bg-white/10"
|
||||||
|
/>
|
||||||
|
{isLive && (
|
||||||
|
<span className="absolute -bottom-1 -right-1 px-1.5 py-0.5 text-[10px] font-bold uppercase bg-red-500 text-white rounded">
|
||||||
|
Live
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h2 className="text-lg font-bold text-white truncate">{displayName}</h2>
|
||||||
|
<p className="text-sm text-white/60">@{user.username}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bio */}
|
||||||
|
{user.bio && (
|
||||||
|
<p className="mt-3 text-sm text-white/80 leading-relaxed">{user.bio}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Meta info */}
|
||||||
|
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-sm text-white/50">
|
||||||
|
{user.location && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<MapPin className="w-3.5 h-3.5" />
|
||||||
|
{user.location}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{user.website && (
|
||||||
|
<a
|
||||||
|
href={user.website.startsWith("http") ? user.website : `https://${user.website}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-teal-400 hover:underline"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
|
{user.website.replace(/^https?:\/\//, "")}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{user.joinedAt && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Calendar className="w-3.5 h-3.5" />
|
||||||
|
Joined {new Date(user.joinedAt).toLocaleDateString("en-US", { month: "short", year: "numeric" })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
{isLive && viewerCount !== undefined && (
|
||||||
|
<div className="mt-3 flex items-center gap-2 text-sm">
|
||||||
|
<span className="flex items-center gap-1.5 text-white/70">
|
||||||
|
<Users className="w-4 h-4 text-red-400" />
|
||||||
|
<span className="font-medium text-white">{viewerCount}</span> watching
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Children (Chat, etc.) */}
|
||||||
|
<div className="flex-1 min-h-0 flex flex-col">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,20 +2,16 @@ import { useEffect, useRef, useState } from "react"
|
|||||||
import { createFileRoute, Link } from "@tanstack/react-router"
|
import { createFileRoute, Link } from "@tanstack/react-router"
|
||||||
import { getStreamByUsername, type StreamPageData } from "@/lib/stream/db"
|
import { getStreamByUsername, type StreamPageData } from "@/lib/stream/db"
|
||||||
import { VideoPlayer } from "@/components/VideoPlayer"
|
import { VideoPlayer } from "@/components/VideoPlayer"
|
||||||
import { CloudflareStreamPlayer } from "@/components/CloudflareStreamPlayer"
|
|
||||||
import { WebRTCPlayer } from "@/components/WebRTCPlayer"
|
|
||||||
import { LiveNowSidebar } from "@/components/LiveNowSidebar"
|
|
||||||
import { resolveStreamPlayback } from "@/lib/stream/playback"
|
import { resolveStreamPlayback } from "@/lib/stream/playback"
|
||||||
import { JazzProvider } from "@/lib/jazz/provider"
|
import { JazzProvider } from "@/lib/jazz/provider"
|
||||||
import { ViewerCount } from "@/components/ViewerCount"
|
|
||||||
import { CommentBox } from "@/components/CommentBox"
|
import { CommentBox } from "@/components/CommentBox"
|
||||||
|
import { ProfileSidebar } from "@/components/ProfileSidebar"
|
||||||
import {
|
import {
|
||||||
getSpotifyNowPlaying,
|
getSpotifyNowPlaying,
|
||||||
type SpotifyNowPlayingResponse,
|
type SpotifyNowPlayingResponse,
|
||||||
} from "@/lib/spotify/now-playing"
|
} from "@/lib/spotify/now-playing"
|
||||||
import { getStreamStatus } from "@/lib/stream/status"
|
|
||||||
import { authClient } from "@/lib/auth-client"
|
import { authClient } from "@/lib/auth-client"
|
||||||
import { MessageCircle, LogIn, X } from "lucide-react"
|
import { MessageCircle, LogIn, X, User } from "lucide-react"
|
||||||
|
|
||||||
export const Route = createFileRoute("/$username")({
|
export const Route = createFileRoute("/$username")({
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@@ -29,9 +25,13 @@ function makeNikivData(hlsUrl: string): StreamPageData {
|
|||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
id: "nikiv",
|
id: "nikiv",
|
||||||
name: "Nikita",
|
name: "Nikita Voloboev",
|
||||||
username: "nikiv",
|
username: "nikiv",
|
||||||
image: null,
|
image: "https://nikiv.dev/nikiv.jpg",
|
||||||
|
bio: "Building in public. Making tools I want to exist.",
|
||||||
|
website: "nikiv.dev",
|
||||||
|
location: null,
|
||||||
|
joinedAt: "2024-01-01",
|
||||||
},
|
},
|
||||||
stream: {
|
stream: {
|
||||||
id: "nikiv-stream",
|
id: "nikiv-stream",
|
||||||
@@ -77,12 +77,8 @@ function StreamPage() {
|
|||||||
let isActive = true
|
let isActive = true
|
||||||
|
|
||||||
// Special handling for nikiv - URL comes from API (secret)
|
// Special handling for nikiv - URL comes from API (secret)
|
||||||
|
// Data and loading state are handled by the HLS check effect
|
||||||
if (username === "nikiv") {
|
if (username === "nikiv") {
|
||||||
// Data will be set when we get the HLS URL from the API
|
|
||||||
if (hlsUrl) {
|
|
||||||
setData(makeNikivData(hlsUrl))
|
|
||||||
}
|
|
||||||
setLoading(false)
|
|
||||||
return () => {
|
return () => {
|
||||||
isActive = false
|
isActive = false
|
||||||
}
|
}
|
||||||
@@ -190,6 +186,7 @@ function StreamPage() {
|
|||||||
if (username !== "nikiv") return
|
if (username !== "nikiv") return
|
||||||
|
|
||||||
let isActive = true
|
let isActive = true
|
||||||
|
let isFirstCheck = true
|
||||||
|
|
||||||
const checkHlsViaApi = async () => {
|
const checkHlsViaApi = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -219,6 +216,12 @@ function StreamPage() {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Silently ignore errors - don't change state on network issues
|
// Silently ignore errors - don't change state on network issues
|
||||||
|
} finally {
|
||||||
|
// Mark loading as done after first check completes
|
||||||
|
if (isActive && isFirstCheck) {
|
||||||
|
isFirstCheck = false
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user