Initial commit — OFApp client + server
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Routes, Route, NavLink, useLocation } from 'react-router-dom'
|
||||
import { getMe } from './api'
|
||||
import Login from './pages/Login'
|
||||
import Feed from './pages/Feed'
|
||||
import Users from './pages/Users'
|
||||
import UserPosts from './pages/UserPosts'
|
||||
import Downloads from './pages/Downloads'
|
||||
import Search from './pages/Search'
|
||||
import Gallery from './pages/Gallery'
|
||||
|
||||
const navItems = [
|
||||
{ to: '/feed', label: 'Feed', icon: FeedIcon },
|
||||
{ to: '/users', label: 'Users', icon: UsersIcon },
|
||||
{ to: '/search', label: 'Search', icon: SearchIcon },
|
||||
{ to: '/downloads', label: 'Downloads', icon: DownloadIcon },
|
||||
{ to: '/gallery', label: 'Gallery', icon: GalleryNavIcon },
|
||||
{ to: '/', label: 'Settings', icon: SettingsIcon },
|
||||
]
|
||||
|
||||
export default function App() {
|
||||
const [currentUser, setCurrentUser] = useState(null)
|
||||
const location = useLocation()
|
||||
|
||||
useEffect(() => {
|
||||
getMe().then((data) => {
|
||||
if (!data.error) {
|
||||
setCurrentUser(data)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const refreshUser = () => {
|
||||
getMe().then((data) => {
|
||||
if (!data.error) {
|
||||
setCurrentUser(data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-[#0a0a0a]">
|
||||
{/* Sidebar */}
|
||||
<aside className="fixed left-0 top-0 bottom-0 w-60 bg-[#111] border-r border-[#222] flex flex-col z-50">
|
||||
{/* Logo */}
|
||||
<div className="p-6 border-b border-[#222]">
|
||||
<h1 className="text-xl font-bold text-white tracking-tight">
|
||||
<span className="text-[#0095f6]">OF</span>App
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 py-4 px-3">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
const isActive =
|
||||
item.to === '/'
|
||||
? location.pathname === '/'
|
||||
: location.pathname.startsWith(item.to)
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg mb-1 transition-all duration-200 ${
|
||||
isActive
|
||||
? 'bg-[#0095f6]/10 text-[#0095f6]'
|
||||
: 'text-gray-400 hover:text-white hover:bg-[#1a1a1a]'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
<span className="text-sm font-medium">{item.label}</span>
|
||||
</NavLink>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Current User */}
|
||||
{currentUser && (
|
||||
<div className="p-4 border-t border-[#222]">
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src={currentUser.avatar}
|
||||
alt={currentUser.name}
|
||||
className="w-9 h-9 rounded-full object-cover bg-[#1a1a1a]"
|
||||
onError={(e) => {
|
||||
e.target.src = `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><rect fill="%23333" width="40" height="40" rx="20"/><text x="20" y="25" text-anchor="middle" fill="white" font-size="16">${(currentUser.name || '?')[0]}</text></svg>`
|
||||
}}
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">
|
||||
{currentUser.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 truncate">
|
||||
@{currentUser.username}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="ml-60 flex-1 min-h-screen">
|
||||
<div className="max-w-5xl mx-auto p-6">
|
||||
<Routes>
|
||||
<Route path="/" element={<Login onAuth={refreshUser} />} />
|
||||
<Route path="/feed" element={<Feed />} />
|
||||
<Route path="/users" element={<Users />} />
|
||||
<Route path="/users/:userId" element={<UserPosts />} />
|
||||
<Route path="/search" element={<Search />} />
|
||||
<Route path="/downloads" element={<Downloads />} />
|
||||
<Route path="/gallery" element={<Gallery />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* Icon Components */
|
||||
|
||||
function FeedIcon({ className }) {
|
||||
return (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function UsersIcon({ className }) {
|
||||
return (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function DownloadIcon({ className }) {
|
||||
return (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function SearchIcon({ className }) {
|
||||
return (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function GalleryNavIcon({ className }) {
|
||||
return (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M18 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsIcon({ className }) {
|
||||
return (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user