Files
gunbuilder-next-tailwind/src/app/build/page.tsx

574 lines
22 KiB
TypeScript
Raw Normal View History

2025-06-29 07:12:20 -04:00
'use client';
import { useState } from 'react';
import Link from 'next/link';
import React from 'react';
// AR-15 Build Requirements grouped by main categories
const buildGroups = [
{
name: 'Upper Parts',
description: 'Components that make up the upper receiver assembly',
components: [
{
id: 'upper-receiver',
name: 'Upper Receiver',
category: 'Upper',
description: 'The upper receiver houses the barrel, bolt carrier group, and charging handle',
required: true,
status: 'pending',
estimatedPrice: 150,
notes: 'Can be purchased as complete upper or stripped'
},
{
id: 'barrel',
name: 'Barrel',
category: 'Upper',
description: 'The barrel determines accuracy and caliber compatibility',
required: true,
status: 'pending',
estimatedPrice: 200,
notes: 'Common lengths: 16", 18", 20"'
},
{
id: 'bolt-carrier-group',
name: 'Bolt Carrier Group (BCG)',
category: 'Upper',
description: 'Handles the firing, extraction, and ejection of rounds',
required: true,
status: 'pending',
estimatedPrice: 150,
notes: 'Mil-spec or enhanced options available'
},
{
id: 'charging-handle',
name: 'Charging Handle',
category: 'Upper',
description: 'Allows manual operation of the bolt carrier group',
required: true,
status: 'pending',
estimatedPrice: 50,
notes: 'Standard or ambidextrous options'
},
{
id: 'gas-block',
name: 'Gas Block',
category: 'Upper',
description: 'Controls gas flow from barrel to BCG',
required: true,
status: 'pending',
estimatedPrice: 30,
notes: 'Low-profile for free-float handguards'
},
{
id: 'gas-tube',
name: 'Gas Tube',
category: 'Upper',
description: 'Transfers gas from barrel to BCG',
required: true,
status: 'pending',
estimatedPrice: 15,
notes: 'Carbine, mid-length, or rifle length'
},
{
id: 'handguard',
name: 'Handguard',
category: 'Upper',
description: 'Provides grip and mounting points for accessories',
required: true,
status: 'pending',
estimatedPrice: 100,
notes: 'Free-float or drop-in options'
},
{
id: 'muzzle-device',
name: 'Muzzle Device',
category: 'Upper',
description: 'Flash hider, compensator, or suppressor mount',
required: true,
status: 'pending',
estimatedPrice: 80,
notes: 'A2 flash hider is standard'
}
]
},
{
name: 'Lower Parts',
description: 'Components that make up the lower receiver assembly',
components: [
{
id: 'lower-receiver',
name: 'Lower Receiver',
category: 'Lower',
description: 'The lower receiver contains the trigger group and magazine well',
required: true,
status: 'pending',
estimatedPrice: 100,
notes: 'Must be purchased through FFL dealer'
},
{
id: 'trigger',
name: 'Trigger',
category: 'Lower',
description: 'Controls firing mechanism',
required: true,
status: 'pending',
estimatedPrice: 60,
notes: 'Mil-spec or enhanced triggers available'
},
{
id: 'trigger-guard',
name: 'Trigger Guard',
category: 'Lower',
description: 'Protects trigger from accidental discharge',
required: true,
status: 'pending',
estimatedPrice: 10,
notes: 'Often included with lower receiver'
},
{
id: 'pistol-grip',
name: 'Pistol Grip',
category: 'Lower',
description: 'Provides grip for firing hand',
required: true,
status: 'pending',
estimatedPrice: 25,
notes: 'Various ergonomic options available'
},
{
id: 'buffer-tube',
name: 'Buffer Tube',
category: 'Lower',
description: 'Houses buffer and spring for recoil management',
required: true,
status: 'pending',
estimatedPrice: 40,
notes: 'Carbine, A5, or rifle length'
},
{
id: 'buffer',
name: 'Buffer',
category: 'Lower',
description: 'Absorbs recoil energy',
required: true,
status: 'pending',
estimatedPrice: 20,
notes: 'H1, H2, H3 weights available'
},
{
id: 'buffer-spring',
name: 'Buffer Spring',
category: 'Lower',
description: 'Returns BCG to battery position',
required: true,
status: 'pending',
estimatedPrice: 15,
notes: 'Standard or enhanced springs'
},
{
id: 'stock',
name: 'Stock',
category: 'Lower',
description: 'Provides shoulder support and cheek weld',
required: true,
status: 'pending',
estimatedPrice: 60,
notes: 'Fixed or adjustable options'
}
]
},
{
name: 'Accessories',
description: 'Additional components needed for a complete build',
components: [
{
id: 'magazine',
name: 'Magazine',
category: 'Accessory',
description: 'Holds and feeds ammunition',
required: true,
status: 'pending',
estimatedPrice: 15,
notes: '30-round capacity is standard'
},
{
id: 'sights',
name: 'Sights',
category: 'Accessory',
description: 'Iron sights or optic for aiming',
required: true,
status: 'pending',
estimatedPrice: 100,
notes: 'Backup iron sights recommended'
}
]
}
];
// Flatten all components for filtering and sorting
const allComponents = buildGroups.flatMap(group => group.components);
const categories = ["All", "Upper", "Lower", "Accessory"];
type SortField = 'name' | 'category' | 'estimatedPrice' | 'status';
type SortDirection = 'asc' | 'desc';
export default function BuildPage() {
const [sortField, setSortField] = useState<SortField>('name');
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
const [selectedCategory, setSelectedCategory] = useState('All');
const [searchTerm, setSearchTerm] = useState('');
// Filter components
const filteredComponents = allComponents.filter(component => {
if (selectedCategory !== 'All' && component.category !== selectedCategory) {
return false;
}
if (searchTerm && !component.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
!component.description.toLowerCase().includes(searchTerm.toLowerCase())) {
return false;
}
return true;
});
// Sort components
const sortedComponents = [...filteredComponents].sort((a, b) => {
let aValue: any, bValue: any;
if (sortField === 'estimatedPrice') {
aValue = a.estimatedPrice;
bValue = b.estimatedPrice;
} else if (sortField === 'category') {
aValue = a.category.toLowerCase();
bValue = b.category.toLowerCase();
} else if (sortField === 'status') {
aValue = a.status.toLowerCase();
bValue = b.status.toLowerCase();
} else {
aValue = a.name.toLowerCase();
bValue = b.name.toLowerCase();
}
if (sortDirection === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
const handleSort = (field: SortField) => {
if (sortField === field) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection('asc');
}
};
const getSortIcon = (field: SortField) => {
if (sortField !== field) {
return '↕️';
}
return sortDirection === 'asc' ? '↑' : '↓';
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed':
return 'bg-green-100 text-green-800';
case 'pending':
return 'bg-yellow-100 text-yellow-800';
case 'in-progress':
return 'bg-blue-100 text-blue-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const totalEstimatedCost = sortedComponents.reduce((sum, component) => sum + component.estimatedPrice, 0);
const completedCount = sortedComponents.filter(component => component.status === 'completed').length;
const hasActiveFilters = selectedCategory !== 'All' || searchTerm;
return (
<main className="min-h-screen bg-gray-50">
{/* Page Title */}
<div className="bg-white border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<h1 className="text-3xl font-bold text-gray-900">AR-15 Build Checklist</h1>
<p className="text-gray-600 mt-2">Track your build progress and find required components</p>
</div>
</div>
{/* Build Summary */}
<div className="bg-white border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-gray-900">{allComponents.length}</div>
<div className="text-sm text-gray-500">Total Components</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600">{completedCount}</div>
<div className="text-sm text-gray-500">Completed</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-yellow-600">{allComponents.length - completedCount}</div>
<div className="text-sm text-gray-500">Remaining</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-blue-600">${totalEstimatedCost}</div>
<div className="text-sm text-gray-500">Estimated Cost</div>
</div>
</div>
</div>
</div>
{/* Search and Filters */}
<div className="bg-white border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
{/* Search Row */}
<div className="mb-4 flex justify-end">
<div className="w-1/2">
<label className="block text-sm font-medium text-gray-700 mb-1">Search Components</label>
<input
type="text"
placeholder="Search components..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
{/* Filters Row */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{/* Category Dropdown */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
{categories.map((category) => (
<option key={category} value={category}>
{category}
</option>
))}
</select>
</div>
{/* Status Filter */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="all">All Status</option>
<option value="pending">Pending</option>
<option value="in-progress">In Progress</option>
<option value="completed">Completed</option>
</select>
</div>
{/* Sort by */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Sort By</label>
<select
value={sortField}
onChange={(e) => handleSort(e.target.value as SortField)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="name">Name</option>
<option value="category">Category</option>
<option value="estimatedPrice">Price</option>
<option value="status">Status</option>
</select>
</div>
{/* Clear Filters */}
<div className="flex items-end">
<button className="w-full px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors">
Clear Filters
</button>
</div>
</div>
</div>
</div>
{/* Build Components Table */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
onClick={() => handleSort('name')}
>
<div className="flex items-center space-x-1">
<span>Component</span>
<span className="text-sm">{getSortIcon('name')}</span>
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
onClick={() => handleSort('category')}
>
<div className="flex items-center space-x-1">
<span>Category</span>
<span className="text-sm">{getSortIcon('category')}</span>
</div>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Description
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
onClick={() => handleSort('estimatedPrice')}
>
<div className="flex items-center space-x-1">
<span>Est. Price</span>
<span className="text-sm">{getSortIcon('estimatedPrice')}</span>
</div>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Notes
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{sortedComponents.length > 0 ? (
buildGroups.map((group) => {
// Filter components in this group that match current filters
const groupComponents = group.components.filter(component =>
sortedComponents.some(sorted => sorted.id === component.id)
);
if (groupComponents.length === 0) return null;
return (
<React.Fragment key={group.name}>
{/* Group Header */}
<tr className="bg-gray-50">
<td colSpan={7} className="px-6 py-4">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
<span className="text-blue-600 font-semibold text-sm">
{group.name === 'Upper Parts' ? '🔫' :
group.name === 'Lower Parts' ? '🔧' : '📦'}
</span>
</div>
</div>
<div className="ml-4">
<h3 className="text-lg font-semibold text-gray-900">{group.name}</h3>
<p className="text-sm text-gray-500">{group.description}</p>
</div>
<div className="ml-auto text-right">
<div className="text-sm text-gray-500">
{groupComponents.length} components
</div>
</div>
</div>
</td>
</tr>
{/* Group Components */}
{groupComponents.map((component) => (
<tr key={component.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(component.status)}`}>
{component.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{component.name}
</div>
<div className="text-xs text-gray-500">
{component.required ? 'Required' : 'Optional'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{component.category}
</span>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-500 max-w-xs">
{component.description}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-semibold text-gray-900">
${component.estimatedPrice}
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-500 max-w-xs">
{component.notes}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex space-x-2">
<Link
href={`/?category=${encodeURIComponent(component.category)}`}
className="bg-blue-600 text-white py-1 px-3 rounded text-xs hover:bg-blue-700 transition-colors"
>
Find Parts
</Link>
<button className="bg-gray-100 text-gray-700 py-1 px-2 rounded text-xs hover:bg-gray-200 transition-colors">
</button>
</div>
</td>
</tr>
))}
</React.Fragment>
);
})
) : (
<tr>
<td colSpan={7} className="px-6 py-12 text-center">
<div className="text-gray-500">
<div className="text-lg font-medium mb-2">No components found</div>
<div className="text-sm">Try adjusting your filters or search terms</div>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Table Footer */}
<div className="bg-gray-50 px-6 py-3 border-t border-gray-200">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-700">
Showing {sortedComponents.length} of {allComponents.length} components
{hasActiveFilters && (
<span className="ml-2 text-blue-600">
(filtered)
</span>
)}
</div>
<div className="text-sm text-gray-500">
Total Value: ${sortedComponents.reduce((sum, component) => sum + component.estimatedPrice, 0).toFixed(2)}
</div>
</div>
</div>
</div>
</div>
</main>
);
}