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,
})
// Feature flag: enable billing section
const BILLING_ENABLED = 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("")
// Filter settings
const [allowedApps, setAllowedApps] = useState([])
const [blockedApps, setBlockedApps] = useState([])
const [audioApps, setAudioApps] = useState([])
const [filterVersion, setFilterVersion] = useState(0)
const [filterSaving, setFilterSaving] = useState(false)
const [filterSaved, setFilterSaved] = useState(false)
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)
}
}
const fetchFilterConfig = async () => {
try {
const res = await fetch("/api/stream-filter")
if (res.ok) {
const data = await res.json()
setAllowedApps(data.allowedApps || [])
setBlockedApps(data.blockedApps || [])
setAudioApps(data.audioApps || [])
setFilterVersion(data.version || 0)
}
} catch {
// Ignore errors
}
}
fetchSettings()
fetchFilterConfig()
}, [])
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 handleFilterSave = async () => {
setFilterSaving(true)
setFilterSaved(false)
try {
const res = await fetch("/api/stream-filter", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
allowedApps,
blockedApps,
audioApps,
}),
})
if (res.ok) {
const data = await res.json()
setFilterVersion(data.version)
setFilterSaved(true)
setTimeout(() => setFilterSaved(false), 2000)
}
} catch {
// Ignore errors
} finally {
setFilterSaving(false)
}
}
const streamUrl = username ? `https://linsa.io/${username}` : null
return (
Beta
Streaming is currently in beta. Features may change and some functionality is still being developed.
{loading ? (
) : (
<>
Enter your Cloudflare Live Input UID to enable automatic stream detection.
Your stream will go live automatically when you start streaming.
Control which apps appear in your stream. Changes apply live without restart.
Config version: {filterVersion}
{filterSaved && Saved}
{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" && BILLING_ENABLED ? (
) : null}
{showEmailModal ? (
setShowEmailModal(false)}
>
) : null}
>
)
}