Created
November 29, 2024 02:54
-
-
Save airstrike/e0e47eaab733277b537923c4d396a15b to your computer and use it in GitHub Desktop.
Permafrost Archive Manager
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Permafrost.jsx | |
* | |
* A React component that provides a modern, interactive UI for viewing and managing | |
* archive files (ZIP, etc.). Features include: | |
* | |
* - File/folder browsing with sorting capabilities | |
* - Detailed metadata sidebar | |
* - MacOS-style window controls | |
* - File type icons and size formatting | |
* - Responsive layout with collapsible sidebar | |
* - Interactive table with hover states | |
* | |
* The component uses Tailwind CSS for styling and Lucide icons for the UI elements. | |
* It supports viewing archive contents with basic metadata like file sizes, modification | |
* dates, and compression details. | |
* | |
* Dependencies: | |
* - React (with useState and useMemo hooks) | |
* - lucide-react for icons | |
* - Tailwind CSS for styling | |
* | |
* @component | |
* @example | |
* return ( | |
* <Permafrost /> | |
* ) | |
*/ | |
import React, { useState, useMemo } from 'react'; | |
import { Folder, FileText, ChevronRight, Download, Menu, X, Minus, Square, Calendar, HardDrive, FileArchive, Shield, Clock, ChevronDown, ChevronUp, Image } from 'lucide-react'; | |
const Button = ({ children, variant = 'default', className = '', ...props }) => ( | |
<button | |
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors | |
${variant === 'primary' ? 'bg-blue-600 hover:bg-blue-700 text-white' : | |
variant === 'ghost' ? 'hover:bg-neutral-700 p-2' : | |
'bg-neutral-700 hover:bg-neutral-600 text-neutral-200'} | |
${className}`} | |
{...props} | |
> | |
{children} | |
</button> | |
); | |
const WindowControls = () => ( | |
<div className="flex gap-2"> | |
<button className="w-3 h-3 rounded-full bg-red-500 hover:bg-red-600"> | |
<X className="w-3 h-3 opacity-0 hover:opacity-50" /> | |
</button> | |
<button className="w-3 h-3 rounded-full bg-yellow-500 hover:bg-yellow-600"> | |
<Minus className="w-3 h-3 opacity-0 hover:opacity-50" /> | |
</button> | |
<button className="w-3 h-3 rounded-full bg-green-500 hover:bg-green-600"> | |
<Square className="w-3 h-3 opacity-0 hover:opacity-50" /> | |
</button> | |
</div> | |
); | |
const TableHeader = ({ label, sortKey, sortConfig, onSort }) => { | |
const getSortIcon = () => { | |
if (sortConfig.key === sortKey) { | |
return sortConfig.direction === 'asc' ? | |
<ChevronUp className="w-3 h-3" /> : | |
<ChevronDown className="w-3 h-3" />; | |
} | |
return null; | |
}; | |
return ( | |
<th | |
className="py-2 sticky top-0 bg-neutral-900 cursor-pointer group" | |
onClick={() => onSort(sortKey)} | |
> | |
<div className="flex items-center gap-1 text-xs text-neutral-400"> | |
{label} | |
<span className="opacity-0 group-hover:opacity-100"> | |
{getSortIcon() || <ChevronUp className="w-3 h-3" />} | |
</span> | |
</div> | |
</th> | |
); | |
}; | |
const MetadataItem = ({ icon: Icon, label, value }) => ( | |
<div className="flex items-center gap-2 py-1.5 border-b border-neutral-800"> | |
<Icon className="w-4 h-4 text-neutral-400" /> | |
<div> | |
<div className="text-xs text-neutral-500">{label}</div> | |
<div className="text-sm text-neutral-200">{value}</div> | |
</div> | |
</div> | |
); | |
const FileIcon = ({ type, kind }) => { | |
if (type === 'folder') return <Folder className="w-4 h-4 text-blue-400 flex-shrink-0" />; | |
if (kind === 'Image') return <Image className="w-4 h-4 text-purple-400 flex-shrink-0" />; | |
return <FileText className="w-4 h-4 text-neutral-400 flex-shrink-0" />; | |
}; | |
const Permafrost = () => { | |
const [isSidebarOpen, setIsSidebarOpen] = useState(false); | |
const [sortConfig, setSortConfig] = useState({ key: 'name', direction: 'asc' }); | |
const filename = 'Celer_34.zip'; | |
const files = useMemo(() => { | |
const baseFiles = [ | |
{ name: 'menu', size: '6.4 kB', sizeBytes: 6400, type: 'folder', kind: 'Folder', modified: '22 Jan 2021' }, | |
{ name: 'lua', size: '203.0 kB', sizeBytes: 203000, type: 'folder', kind: 'Folder', modified: '13 Feb 2021' }, | |
{ name: 'loc', size: '2.2 kB', sizeBytes: 2200, type: 'folder', kind: 'Folder', modified: '22 Jan 2021' }, | |
{ name: 'assets', size: '51.5 kB', sizeBytes: 51500, type: 'folder', kind: 'Folder', modified: '14 Feb 2021' }, | |
{ name: 'tdlq.dds', size: '16.5 kB', sizeBytes: 16500, type: 'file', kind: 'Image', modified: '12 Nov 2020' }, | |
{ name: 'screenshot.png', size: '245 kB', sizeBytes: 245000, type: 'file', kind: 'Image', modified: '12 Nov 2020' }, | |
{ name: 'splash.jpg', size: '123 kB', sizeBytes: 123000, type: 'file', kind: 'Image', modified: '12 Nov 2020' }, | |
{ name: 'mod.txt', size: '864 bytes', sizeBytes: 864, type: 'file', kind: 'Text', modified: '28 Jun 2021' } | |
]; | |
const fileTypes = ['Text', 'Document', 'Image', 'Archive']; | |
return [...baseFiles, ...Array(15).fill(null).map((_, i) => { | |
const sizeBytes = Math.floor(Math.random() * 1000000); | |
const type = 'file'; | |
const kind = fileTypes[Math.floor(Math.random() * fileTypes.length)]; | |
return { | |
name: `file-${i + 1}${kind === 'Image' ? '.png' : kind === 'Document' ? '.pdf' : '.txt'}`, | |
size: `${Math.floor(sizeBytes / 1024)} kB`, | |
sizeBytes, | |
type, | |
kind, | |
modified: '28 Jun 2021' | |
}; | |
})]; | |
}, []); | |
const metadata = { | |
filename, | |
created: '22 January 2021, 18:13', | |
modified: '28 June 2021, 09:55', | |
size: '282.5 kB', | |
format: 'ZIP Archive', | |
compression: 'Deflate', | |
crc: 'CRC32', | |
encrypted: 'No' | |
}; | |
const sortData = (data) => { | |
return [...data].sort((a, b) => { | |
// Always sort folders first | |
if (a.type !== b.type) { | |
return a.type === 'folder' ? -1 : 1; | |
} | |
if (sortConfig.key === 'size') { | |
return sortConfig.direction === 'asc' ? | |
a.sizeBytes - b.sizeBytes : | |
b.sizeBytes - a.sizeBytes; | |
} | |
if (sortConfig.direction === 'asc') { | |
return a[sortConfig.key].localeCompare(b[sortConfig.key]); | |
} | |
return b[sortConfig.key].localeCompare(a[sortConfig.key]); | |
}); | |
}; | |
const handleSort = (key) => { | |
setSortConfig(current => ({ | |
key, | |
direction: current.key === key && current.direction === 'asc' ? 'desc' : 'asc', | |
})); | |
}; | |
const sortedFiles = sortData(files); | |
return ( | |
<div className="min-h-screen bg-gradient-to-br from-indigo-950 to-indigo-900 flex items-center justify-center p-8"> | |
<div className="w-full max-w-4xl bg-neutral-900 rounded-lg shadow-xl overflow-hidden relative h-[500px]"> | |
{/* Header */} | |
<div className="px-4 py-2 bg-neutral-800 flex justify-between items-center"> | |
<div className="flex items-center gap-3"> | |
<WindowControls /> | |
<span className="text-sm font-medium text-neutral-200">Permafrost: {filename}</span> | |
</div> | |
</div> | |
{/* Main Content */} | |
<div className="flex flex-col h-[calc(100%-32px)]"> | |
{/* Toolbar */} | |
<div className="px-4 pt-4 pb-2"> | |
<div className="flex justify-between items-center"> | |
<Button variant="primary" className="flex items-center gap-2"> | |
<Download className="h-4 w-4" /> | |
Extract Files | |
</Button> | |
{!isSidebarOpen && ( | |
<Button | |
variant="ghost" | |
onClick={() => setIsSidebarOpen(true)} | |
className="text-neutral-400" | |
> | |
<Menu className="h-5 w-5" /> | |
</Button> | |
)} | |
</div> | |
</div> | |
{/* Table Container */} | |
<div className="flex-1 overflow-auto px-4"> | |
<table className="w-full"> | |
<thead> | |
<tr className="text-left"> | |
<TableHeader label="Name" sortKey="name" sortConfig={sortConfig} onSort={handleSort} /> | |
<TableHeader label="Kind" sortKey="kind" sortConfig={sortConfig} onSort={handleSort} className="w-[15%]" /> | |
<TableHeader label="Size" sortKey="size" sortConfig={sortConfig} onSort={handleSort} className="w-[15%]" /> | |
<TableHeader label="Modified" sortKey="modified" sortConfig={sortConfig} onSort={handleSort} className="w-[20%]" /> | |
</tr> | |
</thead> | |
<tbody className="relative"> | |
{sortedFiles.map((file, index) => ( | |
<tr key={index} className="hover:bg-neutral-800 group"> | |
<td className="py-1"> | |
<div className="flex items-center space-x-2"> | |
<FileIcon type={file.type} kind={file.kind} /> | |
<span className="text-sm text-neutral-200 truncate">{file.name}</span> | |
{file.type === 'folder' && ( | |
<ChevronRight className="w-3 h-3 text-neutral-500 opacity-0 group-hover:opacity-100 flex-shrink-0" /> | |
)} | |
</div> | |
</td> | |
<td className="py-1 text-xs text-neutral-400">{file.kind}</td> | |
<td className="py-1 text-xs text-neutral-400">{file.size}</td> | |
<td className="py-1 text-xs text-neutral-400">{file.modified}</td> | |
</tr> | |
))} | |
<tr> | |
<td colSpan="4" className="h-8"></td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
{/* Footer */} | |
<div className="px-8 py-2 bg-neutral-900 border-t border-neutral-800 flex justify-between text-xs text-neutral-400"> | |
<div className="flex gap-8"> | |
<div> | |
<span className="text-neutral-500">Total Size:</span> {metadata.size} | |
</div> | |
<div> | |
<span className="text-neutral-500">Items:</span> {files.length} | |
</div> | |
</div> | |
<div> | |
<span className="text-neutral-500">Type:</span> {metadata.format} | |
</div> | |
</div> | |
</div> | |
{/* Sidebar */} | |
<div | |
className={`absolute top-0 right-0 w-72 h-full border-l border-neutral-800 bg-neutral-900 shadow-xl transition-transform duration-300 ease-in-out ${ | |
isSidebarOpen ? 'translate-x-0' : 'translate-x-full' | |
}`} | |
> | |
<div className="p-4"> | |
<div className="flex justify-between items-center mb-2"> | |
<h3 className="text-sm font-medium text-neutral-200">Archive Information</h3> | |
<Button | |
variant="ghost" | |
onClick={() => setIsSidebarOpen(false)} | |
className="p-1 hover:bg-neutral-800" | |
> | |
<X className="w-4 h-4 text-neutral-400" /> | |
</Button> | |
</div> | |
<div className="space-y-0"> | |
<MetadataItem icon={FileArchive} label="Filename" value={metadata.filename} /> | |
<MetadataItem icon={Calendar} label="Created" value={metadata.created} /> | |
<MetadataItem icon={Clock} label="Modified" value={metadata.modified} /> | |
<MetadataItem icon={HardDrive} label="Size" value={metadata.size} /> | |
<MetadataItem icon={FileArchive} label="Format" value={metadata.format} /> | |
<MetadataItem icon={FileArchive} label="Compression" value={metadata.compression} /> | |
<MetadataItem icon={Shield} label="CRC" value={metadata.crc} /> | |
<MetadataItem icon={Shield} label="Encrypted" value={metadata.encrypted} /> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
); | |
}; | |
export default Permafrost; |
Author
airstrike
commented
Nov 29, 2024
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment