import { useEffect, useMemo, useState, type FormEvent, type ReactNode } from "react"
import { createFileRoute } from "@tanstack/react-router"
import { authClient } from "@/lib/auth-client"
import SettingsPanel from "@/components/Settings-panel"
import {
Check,
ChevronDown,
LogOut,
Sparkles,
UserRoundPen,
MessageCircle,
HelpCircle,
Copy,
ExternalLink,
} from "lucide-react"
type SectionId = "preferences" | "profile" | "streaming" | "billing"
const PLAN_CARD_NOISE =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.18'/%3E%3C/svg%3E"
export const Route = createFileRoute("/settings")({
component: SettingsPage,
ssr: false,
})
type Option = { value: string; label: string }
function InlineSelect({
value,
options,
onChange,
}: {
value: string
options: Option[]
onChange: (value: string) => void
}) {
return (
)
}
function ToggleSwitch({
checked,
onChange,
}: {
checked: boolean
onChange: (next: boolean) => void
}) {
return (
)
}
function SettingRow({
title,
description,
control,
}: {
title: string
description: string
control?: ReactNode
}) {
return (
{control ?
{control}
: null}
)
}
function SettingCard({
title,
children,
}: {
title: string
children: ReactNode
}) {
return (
)
}
function Modal({
title,
description,
onClose,
children,
}: {
title: string
description?: string
onClose: () => void
children: ReactNode
}) {
return (
e.stopPropagation()}
>
{title}
{description ? (
{description}
) : null}
{children}
)
}
function SectionHeader({
title,
description,
}: {
title: string
description?: string
}) {
return (
{title}
{description ? (
{description}
) : null}
)
}
function PreferencesSection() {
const [theme, setTheme] = useState("Dark")
const [autoplay, setAutoplay] = useState(true)
const [lowLatency, setLowLatency] = useState(true)
const [chatTimestamps, setChatTimestamps] = useState(false)
return (
)
}
function ProfileSection({
profile: sessionProfile,
onLogout,
onChangeEmail,
}: {
profile: { name?: string | null; email: string; username?: string | null; image?: string | null; bio?: string | null; website?: string | null } | null | undefined
onLogout: () => Promise
onChangeEmail: () => void
}) {
const [loading, setLoading] = useState(true)
const [profile, setProfile] = useState(sessionProfile)
const [username, setUsername] = useState("")
const [name, setName] = useState("")
const [image, setImage] = useState("")
const [bio, setBio] = useState("")
const [website, setWebsite] = useState("")
const [saving, setSaving] = useState(false)
const [error, setError] = useState(null)
const [saved, setSaved] = useState(false)
// Fetch full profile from API on mount
useEffect(() => {
const fetchProfile = async () => {
try {
const res = await fetch("/api/profile", { credentials: "include" })
if (res.ok) {
const data = await res.json()
setProfile(data)
setUsername(data.username ?? "")
setName(data.name ?? "")
setImage(data.image ?? "")
setBio(data.bio ?? "")
setWebsite(data.website ?? "")
}
} catch {
// Fall back to session data
setUsername(sessionProfile?.username ?? "")
setName(sessionProfile?.name ?? "")
setImage(sessionProfile?.image ?? "")
setBio(sessionProfile?.bio ?? "")
setWebsite(sessionProfile?.website ?? "")
} finally {
setLoading(false)
}
}
fetchProfile()
}, [sessionProfile])
const initials = useMemo(() => {
if (!profile) return "G"
return (
profile.name?.slice(0, 1) ??
profile.email?.slice(0, 1)?.toUpperCase() ??
"G"
)
}, [profile])
const avatarUrl = image || `https://api.dicebear.com/7.x/initials/svg?seed=${username || profile?.email || "user"}`
const handleSaveProfile = async () => {
setSaving(true)
setError(null)
setSaved(false)
try {
const res = await fetch("/api/profile", {
method: "PUT",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
name,
username: username.toLowerCase(),
image: image || null,
bio: bio || null,
website: website || null,
}),
})
const data = await res.json()
if (!res.ok) {
setError(data.error || "Failed to save")
} else {
setSaved(true)
setTimeout(() => setSaved(false), 2000)
}
} catch {
setError("Network error")
} finally {
setSaving(false)
}
}
const hasChanges =
username !== (profile?.username ?? "") ||
name !== (profile?.name ?? "") ||
image !== (profile?.image ?? "") ||
bio !== (profile?.bio ?? "") ||
website !== (profile?.website ?? "")
if (loading) {
return (
)
}
return (
{initials}
{profile?.name ?? "Guest user"}
{profile?.email ?? "-"}
{/* Profile Picture */}
setName(e.target.value)}
placeholder="Your name"
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-teal-500"
/>
setWebsite(e.target.value)}
placeholder="https://yourwebsite.com"
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-teal-500"
/>
{error &&
{error}
}
{saved && Saved}
Sign out
Sign out of your account on this device.
Get help
Join our Discord community or contact support.
)
}
function StreamingSection({ username }: { username: string | null | undefined }) {
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [saved, setSaved] = useState(false)
const [error, setError] = useState(null)
const [copied, setCopied] = useState(false)
// Stream settings
const [title, setTitle] = useState("")
const [description, setDescription] = useState("")
const [liveInputUid, setLiveInputUid] = useState("")
const [customerCode, setCustomerCode] = useState("")
const [streamKey, setStreamKey] = useState("")
useEffect(() => {
const fetchSettings = async () => {
try {
const res = await fetch("/api/stream/settings", { credentials: "include" })
if (res.ok) {
const data = await res.json()
setTitle(data.title || "")
setDescription(data.description || "")
setLiveInputUid(data.cloudflare_live_input_uid || "")
setCustomerCode(data.cloudflare_customer_code || "")
setStreamKey(data.stream_key || "")
}
} catch {
// Ignore errors
} finally {
setLoading(false)
}
}
fetchSettings()
}, [])
const handleSave = async () => {
setSaving(true)
setError(null)
setSaved(false)
try {
const res = await fetch("/api/stream/settings", {
method: "PUT",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
title,
description,
cloudflare_live_input_uid: liveInputUid || null,
cloudflare_customer_code: customerCode || null,
}),
})
const data = await res.json()
if (!res.ok) {
setError(data.error || "Failed to save")
} else {
setSaved(true)
setTimeout(() => setSaved(false), 2000)
}
} catch {
setError("Network error")
} finally {
setSaving(false)
}
}
const copyStreamKey = () => {
navigator.clipboard.writeText(streamKey)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
const streamUrl = username ? `https://linsa.io/${username}` : null
return (
{loading ? (
) : (
<>
Enter your Cloudflare Live Input UID to enable automatic stream detection.
Your stream will go live automatically when you start streaming.
{streamUrl && (
)}
{streamKey && (
{streamKey.slice(0, 8)}...{streamKey.slice(-4)}
Use this key to stream to Linsa (coming soon).
)}
{error &&
{error}
}
{saved && Saved}
>
)}
)
}
function BillingSection() {
const [isSubscribed, setIsSubscribed] = useState(false)
const [loading, setLoading] = useState(true)
const [subscribing, setSubscribing] = useState(false)
useEffect(() => {
const checkSubscription = async () => {
try {
const res = await fetch("/api/stripe/billing", { credentials: "include" })
if (res.ok) {
const data = await res.json()
setIsSubscribed(data.hasActiveSubscription)
}
} catch {
// Ignore errors
} finally {
setLoading(false)
}
}
checkSubscription()
}, [])
const handleSubscribe = async () => {
setSubscribing(true)
try {
const res = await fetch("/api/stripe/checkout", {
method: "POST",
credentials: "include",
})
const data = await res.json()
if (data.url) {
window.location.href = data.url
}
} catch (err) {
console.error("Failed to start checkout:", err)
} finally {
setSubscribing(false)
}
}
const handleManageBilling = async () => {
try {
const res = await fetch("/api/stripe/portal", {
method: "POST",
credentials: "include",
})
const data = await res.json()
if (data.url) {
window.location.href = data.url
}
} catch (err) {
console.error("Failed to open billing portal:", err)
}
}
return (
{/* Plan Card */}
{isSubscribed && (
Active
)}
Linsa Pro
$8
/ month
-
Unlimited stream replays
-
HD streaming quality
-
Priority support
{loading ? (
) : isSubscribed ? (
) : (
)}
{!isSubscribed && !loading && (
Cancel anytime. No questions asked.
)}
)
}
function SettingsPage() {
const { data: session, isPending } = authClient.useSession()
const [activeSection, setActiveSection] = useState("preferences")
const [showEmailModal, setShowEmailModal] = useState(false)
const [emailInput, setEmailInput] = useState("")
const handleLogout = async () => {
await authClient.signOut()
window.location.href = "/"
}
const openEmailModal = () => {
setEmailInput(session?.user?.email ?? "")
setShowEmailModal(true)
}
const handleEmailSubmit = (event: FormEvent) => {
event.preventDefault()
setShowEmailModal(false)
}
if (isPending) {
return (
)
}
return (
<>
{activeSection === "preferences" ? (
) : activeSection === "profile" ? (
) : activeSection === "streaming" ? (
) : activeSection === "billing" ? (
) : null}
{showEmailModal ? (
setShowEmailModal(false)}
>
) : null}
>
)
}