163 lines
6.8 KiB
TypeScript
163 lines
6.8 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import Head from 'next/head';
|
|
import { Navbar } from '../components/Navbar';
|
|
import { MaterialCard } from '../components/MaterialCard';
|
|
import { MaterialDetail } from '../components/MaterialDetail';
|
|
import { CreateModal } from '../components/CreateModal';
|
|
import { ProfileModal } from '../components/ProfileModal';
|
|
import { DataService } from '../services/dataService';
|
|
import { MaterialDTO, UserDTO, MaterialType } from '../types';
|
|
import { Filter, Grid } from 'lucide-react';
|
|
|
|
export default function Home() {
|
|
const [materials, setMaterials] = useState<MaterialDTO[]>([]);
|
|
const [page, setPage] = useState(1);
|
|
const pageSize = 20;
|
|
const [total, setTotal] = useState(0);
|
|
const [hasNext, setHasNext] = useState(false);
|
|
const [currentUser, setCurrentUser] = useState<UserDTO | null>(null);
|
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
|
const [isProfileOpen, setIsProfileOpen] = useState(false);
|
|
const [filter, setFilter] = useState<'ALL' | 'CODE' | 'VIDEO' | 'ASSET_ZIP'>('ALL');
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
const loadMaterials = useCallback(async () => {
|
|
try {
|
|
const filterKey: MaterialType | 'ALL' = filter === 'ALL' ? 'ALL' : filter as any;
|
|
const result = await DataService.getMaterials(page, pageSize, filterKey, searchQuery);
|
|
setMaterials(result.items);
|
|
setTotal(result.total);
|
|
setHasNext(result.hasNext);
|
|
} catch (e) {
|
|
console.error("Failed to load materials", e);
|
|
}
|
|
}, [page, filter, searchQuery]);
|
|
|
|
const refreshUser = useCallback(async () => {
|
|
const user = await DataService.getCurrentUser();
|
|
setCurrentUser(user);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const run = async () => {
|
|
await refreshUser();
|
|
await loadMaterials();
|
|
};
|
|
run();
|
|
}, [loadMaterials, refreshUser]);
|
|
|
|
const filteredMaterials = materials;
|
|
|
|
return (
|
|
<div className="min-h-screen pb-20">
|
|
<Head>
|
|
<title>NEXUS_MAT.OS</title>
|
|
</Head>
|
|
<Navbar
|
|
user={currentUser}
|
|
onOpenCreate={() => setIsCreateOpen(true)}
|
|
onProfileClick={() => setIsProfileOpen(true)}
|
|
searchQuery={searchQuery}
|
|
onSearch={(q) => { setSearchQuery(q); setPage(1); }}
|
|
/>
|
|
|
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-8">
|
|
|
|
{/* Header / Filter Section */}
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between mb-12 gap-6">
|
|
<div>
|
|
<h1 className="text-4xl md:text-5xl font-bold text-white font-mono tracking-tighter mb-2">
|
|
GRID_ACCESS
|
|
</h1>
|
|
<p className="text-gray-500 font-mono text-sm">
|
|
INDEXING {total} RESOURCES FROM THE NETWORK
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4 bg-cyber-panel/30 p-2 rounded-lg border border-white/5 backdrop-blur-sm">
|
|
<Filter size={16} className="text-cyber-neon ml-2" />
|
|
<div className="flex gap-2">
|
|
{(['ALL', 'CODE', 'VIDEO', 'ASSET_ZIP'] as const).map(f => (
|
|
<button
|
|
key={f}
|
|
onClick={() => { setFilter(f); setPage(1); }}
|
|
className={`px-3 py-1 text-xs font-mono rounded transition-all ${filter === f
|
|
? 'bg-cyber-neon text-black font-bold'
|
|
: 'text-gray-400 hover:text-white'
|
|
}`}
|
|
>
|
|
{f}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="w-[1px] h-6 bg-gray-700 mx-2"></div>
|
|
<Grid size={16} className="text-gray-400" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* The Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{filteredMaterials.map(m => (
|
|
<MaterialCard
|
|
key={m.id}
|
|
material={m}
|
|
onClick={(id) => setSelectedId(id)}
|
|
currentUser={currentUser}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Pagination */}
|
|
<div className="mt-8 flex items-center justify-center gap-4">
|
|
<button onClick={() => setPage(p => Math.max(1, p - 1))} className="px-3 py-2 border border-white/10 text-gray-400 hover:text-white">Prev</button>
|
|
<span className="text-xs font-mono text-gray-500">PAGE {page} / {Math.max(1, Math.ceil(total / pageSize))}</span>
|
|
<button
|
|
onClick={() => setPage(p => p + 1)}
|
|
disabled={!hasNext}
|
|
className="px-3 py-2 border border-white/10 text-gray-400 hover:text-white disabled:opacity-50"
|
|
>Next</button>
|
|
</div>
|
|
|
|
{/* Empty State */}
|
|
{filteredMaterials.length === 0 && (
|
|
<div className="text-center py-20 border border-dashed border-gray-800 rounded-xl">
|
|
<p className="text-gray-600 font-mono">NO DATA FOUND IN SECTOR.</p>
|
|
</div>
|
|
)}
|
|
|
|
</main>
|
|
|
|
{/* Modals */}
|
|
{selectedId && (
|
|
<MaterialDetail
|
|
id={selectedId}
|
|
onClose={() => setSelectedId(null)}
|
|
currentUser={currentUser}
|
|
/>
|
|
)}
|
|
|
|
{isCreateOpen && (
|
|
<CreateModal
|
|
onClose={() => setIsCreateOpen(false)}
|
|
onSuccess={() => {
|
|
setIsCreateOpen(false);
|
|
loadMaterials();
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{isProfileOpen && currentUser && (
|
|
<ProfileModal
|
|
user={currentUser}
|
|
onClose={() => setIsProfileOpen(false)}
|
|
onUpdate={refreshUser}
|
|
/>
|
|
)}
|
|
|
|
{/* Footer Decoration */}
|
|
<footer className="fixed bottom-0 left-0 w-full h-1 bg-gradient-to-r from-cyber-neon via-purple-600 to-cyber-blue opacity-50"></footer>
|
|
</div>
|
|
);
|
|
}
|