Skip to content

Instantly share code, notes, and snippets.

@airstrike
Created November 29, 2024 02:54
Show Gist options
  • Save airstrike/e0e47eaab733277b537923c4d396a15b to your computer and use it in GitHub Desktop.
Save airstrike/e0e47eaab733277b537923c4d396a15b to your computer and use it in GitHub Desktop.
Permafrost Archive Manager
/**
* 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;
@airstrike
Copy link
Author

permafrost

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment