mirror of
https://gitea.gofwd.group/sean/gunbuilder-next-tailwind.git
synced 2025-12-06 02:56:45 -05:00
first commit
This commit is contained in:
574
src/app/build/page.tsx
Normal file
574
src/app/build/page.tsx
Normal file
@@ -0,0 +1,574 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user