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,
Lock,
MessageCircle,
HelpCircle,
} from "lucide-react"
type SectionId = "preferences" | "profile" | "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 [homeView, setHomeView] = useState("Active issues")
const [displayFullNames, setDisplayFullNames] = useState(false)
const [firstDay, setFirstDay] = useState("Sunday")
const [convertEmojis, setConvertEmojis] = useState(true)
const [sidebar, setSidebar] = useState("Customize")
const [fontSize, setFontSize] = useState("Default")
const [pointerCursor, setPointerCursor] = useState(false)
const [theme, setTheme] = useState("System preference")
const [lightTheme, setLightTheme] = useState("Light")
const [darkTheme, setDarkTheme] = useState("Dark")
return (
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
)
}
function ProfileSection({
profile,
onLogout,
onChangeEmail,
onChangePassword,
}: {
profile: { name?: string | null; email: string; username?: string | null } | null | undefined
onLogout: () => Promise
onChangeEmail: () => void
onChangePassword: () => void
}) {
const [username, setUsername] = useState(profile?.username ?? "")
const [name, setName] = useState(profile?.name ?? "")
const [saving, setSaving] = useState(false)
const [error, setError] = useState(null)
const [saved, setSaved] = useState(false)
const initials = useMemo(() => {
if (!profile) return "G"
return (
profile.name?.slice(0, 1) ??
profile.email?.slice(0, 1)?.toUpperCase() ??
"G"
)
}, [profile])
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() }),
})
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 ?? "")
return (
{initials}
{profile?.name ?? "Guest user"}
{profile?.email ?? "-"}
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"
/>
{error &&
{error}
}
{saved && Saved}
Change
}
/>
Sign out
Revoke access on this device.
Get help
Join our Discord community or contact support.
)
}
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 bookmark saving
-
Access to all stream archives
-
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 [showPasswordModal, setShowPasswordModal] = useState(false)
const [emailInput, setEmailInput] = useState("")
const [currentPassword, setCurrentPassword] = useState("")
const [newPassword, setNewPassword] = useState("")
const handleLogout = async () => {
await authClient.signOut()
window.location.href = "/"
}
const openEmailModal = () => {
setEmailInput(session?.user?.email ?? "")
setShowEmailModal(true)
}
const openPasswordModal = () => {
setCurrentPassword("")
setNewPassword("")
setShowPasswordModal(true)
}
const handleEmailSubmit = (event: FormEvent) => {
event.preventDefault()
setShowEmailModal(false)
}
const handlePasswordSubmit = (event: FormEvent) => {
event.preventDefault()
setShowPasswordModal(false)
setCurrentPassword("")
setNewPassword("")
}
if (isPending) {
return (
)
}
return (
<>
{activeSection === "preferences" ? (
) : activeSection === "profile" ? (
) : activeSection === "billing" ? (
) : null}
{showEmailModal ? (
setShowEmailModal(false)}
>
) : null}
{showPasswordModal ? (
setShowPasswordModal(false)}
>
) : null}
>
)
}