mirror of
https://gitea.gofwd.group/sean/gunbuilder-next-tailwind.git
synced 2025-12-06 02:56:45 -05:00
styles all fixed
This commit is contained in:
@@ -331,27 +331,15 @@ export default function BuildPage() {
|
|||||||
{/* Search and Filters */}
|
{/* Search and Filters */}
|
||||||
<div className="bg-white border-b">
|
<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="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">
|
|
||||||
<SearchInput
|
|
||||||
label="Search Components"
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={setSearchTerm}
|
|
||||||
placeholder="Search components..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters Row */}
|
{/* Filters Row */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 !bg-white">
|
||||||
{/* Category Dropdown */}
|
{/* Category Dropdown */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
|
||||||
<select
|
<select
|
||||||
value={selectedCategory}
|
value={selectedCategory}
|
||||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
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"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white text-gray-900"
|
||||||
>
|
>
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<option key={category} value={category}>
|
<option key={category} value={category}>
|
||||||
@@ -364,7 +352,7 @@ export default function BuildPage() {
|
|||||||
{/* Status Filter */}
|
{/* Status Filter */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
<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">
|
<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 bg-white text-gray-900">
|
||||||
<option value="all">All Status</option>
|
<option value="all">All Status</option>
|
||||||
<option value="pending">Pending</option>
|
<option value="pending">Pending</option>
|
||||||
<option value="in-progress">In Progress</option>
|
<option value="in-progress">In Progress</option>
|
||||||
@@ -378,7 +366,7 @@ export default function BuildPage() {
|
|||||||
<select
|
<select
|
||||||
value={sortField}
|
value={sortField}
|
||||||
onChange={(e) => handleSort(e.target.value as 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"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white text-gray-900"
|
||||||
>
|
>
|
||||||
<option value="name">Name</option>
|
<option value="name">Name</option>
|
||||||
<option value="category">Category</option>
|
<option value="category">Category</option>
|
||||||
@@ -521,14 +509,11 @@ export default function BuildPage() {
|
|||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<Link
|
<Link
|
||||||
href={`/?category=${encodeURIComponent(component.category)}`}
|
href={`/parts?category=${encodeURIComponent(component.category)}`}
|
||||||
className="bg-blue-600 text-white py-1 px-3 rounded text-xs hover:bg-blue-700 transition-colors"
|
className="bg-[#4B6516] text-white py-1 px-3 rounded text-xs hover:bg-[#3a4e12] transition-colors"
|
||||||
>
|
>
|
||||||
Find Parts
|
Find Parts
|
||||||
</Link>
|
</Link>
|
||||||
<button className="bg-gray-100 text-gray-700 py-1 px-2 rounded text-xs hover:bg-gray-200 transition-colors">
|
|
||||||
✓
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
|||||||
import SearchInput from '@/components/SearchInput';
|
import SearchInput from '@/components/SearchInput';
|
||||||
|
|
||||||
// Sample build data
|
// Sample build data
|
||||||
const sampleBuilds = [
|
const sampleMyBuilds = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
name: 'Budget AR-15 Build',
|
name: 'Budget AR-15 Build',
|
||||||
@@ -121,18 +121,18 @@ const sampleBuilds = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
type BuildStatus = 'completed' | 'in-progress' | 'planning';
|
type MyBuildStatus = 'completed' | 'in-progress' | 'planning';
|
||||||
type SortField = 'name' | 'status' | 'totalCost' | 'completedDate';
|
type SortField = 'name' | 'status' | 'totalCost' | 'completedDate';
|
||||||
type SortDirection = 'asc' | 'desc';
|
type SortDirection = 'asc' | 'desc';
|
||||||
|
|
||||||
export default function BuildsPage() {
|
export default function MyBuildsPage() {
|
||||||
const [sortField, setSortField] = useState<SortField>('completedDate');
|
const [sortField, setSortField] = useState<SortField>('completedDate');
|
||||||
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
|
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
|
||||||
const [selectedStatus, setSelectedStatus] = useState<BuildStatus | 'all'>('all');
|
const [selectedStatus, setSelectedStatus] = useState<MyBuildStatus | 'all'>('all');
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
// Filter builds
|
// Filter builds
|
||||||
const filteredBuilds = sampleBuilds.filter(build => {
|
const filteredMyBuilds = sampleMyBuilds.filter(build => {
|
||||||
if (selectedStatus !== 'all' && build.status !== selectedStatus) {
|
if (selectedStatus !== 'all' && build.status !== selectedStatus) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ export default function BuildsPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Sort builds
|
// Sort builds
|
||||||
const sortedBuilds = [...filteredBuilds].sort((a, b) => {
|
const sortedMyBuilds = [...filteredMyBuilds].sort((a, b) => {
|
||||||
let aValue: any, bValue: any;
|
let aValue: any, bValue: any;
|
||||||
|
|
||||||
if (sortField === 'totalCost') {
|
if (sortField === 'totalCost') {
|
||||||
@@ -170,7 +170,7 @@ export default function BuildsPage() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const getStatusColor = (status: BuildStatus) => {
|
const getStatusColor = (status: MyBuildStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return 'bg-green-100 text-green-800';
|
return 'bg-green-100 text-green-800';
|
||||||
@@ -183,7 +183,7 @@ export default function BuildsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusIcon = (status: BuildStatus) => {
|
const getStatusIcon = (status: MyBuildStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return '✓';
|
return '✓';
|
||||||
@@ -204,14 +204,14 @@ export default function BuildsPage() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProgressPercentage = (build: typeof sampleBuilds[0]) => {
|
const getProgressPercentage = (build: typeof sampleMyBuilds[0]) => {
|
||||||
return Math.round((build.components.completed / build.components.total) * 100);
|
return Math.round((build.components.completed / build.components.total) * 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const totalBuilds = sampleBuilds.length;
|
const totalMyBuilds = sampleMyBuilds.length;
|
||||||
const completedBuilds = sampleBuilds.filter(build => build.status === 'completed').length;
|
const completedMyBuilds = sampleMyBuilds.filter(build => build.status === 'completed').length;
|
||||||
const inProgressBuilds = sampleBuilds.filter(build => build.status === 'in-progress').length;
|
const inProgressMyBuilds = sampleMyBuilds.filter(build => build.status === 'in-progress').length;
|
||||||
const totalValue = sampleBuilds.reduce((sum, build) => sum + build.totalCost, 0);
|
const totalValue = sampleMyBuilds.reduce((sum, build) => sum + build.totalCost, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-gray-50">
|
<main className="min-h-screen bg-gray-50">
|
||||||
@@ -228,15 +228,15 @@ export default function BuildsPage() {
|
|||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
<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="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-gray-900">{totalBuilds}</div>
|
<div className="text-2xl font-bold text-gray-900">{totalMyBuilds}</div>
|
||||||
<div className="text-sm text-gray-500">Total Builds</div>
|
<div className="text-sm text-gray-500">Total Builds</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-green-600">{completedBuilds}</div>
|
<div className="text-2xl font-bold text-green-600">{completedMyBuilds}</div>
|
||||||
<div className="text-sm text-gray-500">Completed</div>
|
<div className="text-sm text-gray-500">Completed</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-blue-600">{inProgressBuilds}</div>
|
<div className="text-2xl font-bold text-blue-600">{inProgressMyBuilds}</div>
|
||||||
<div className="text-sm text-gray-500">In Progress</div>
|
<div className="text-sm text-gray-500">In Progress</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@@ -254,10 +254,10 @@ export default function BuildsPage() {
|
|||||||
<div className="mb-4 flex justify-end">
|
<div className="mb-4 flex justify-end">
|
||||||
<div className="w-1/2">
|
<div className="w-1/2">
|
||||||
<SearchInput
|
<SearchInput
|
||||||
label="Search Builds"
|
label="Search My Builds"
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={setSearchTerm}
|
onChange={setSearchTerm}
|
||||||
placeholder="Search builds..."
|
placeholder="Search my builds..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -269,7 +269,7 @@ export default function BuildsPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
||||||
<select
|
<select
|
||||||
value={selectedStatus}
|
value={selectedStatus}
|
||||||
onChange={(e) => setSelectedStatus(e.target.value as BuildStatus | 'all')}
|
onChange={(e) => setSelectedStatus(e.target.value as MyBuildStatus | 'all')}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
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="all">All Status</option>
|
||||||
@@ -319,9 +319,9 @@ export default function BuildsPage() {
|
|||||||
|
|
||||||
{/* Builds Grid */}
|
{/* Builds Grid */}
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{sortedBuilds.length > 0 ? (
|
{sortedMyBuilds.length > 0 ? (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{sortedBuilds.map((build) => (
|
{sortedMyBuilds.map((build) => (
|
||||||
<div key={build.id} className="bg-white rounded-lg shadow-sm border hover:shadow-md transition-shadow overflow-hidden">
|
<div key={build.id} className="bg-white rounded-lg shadow-sm border hover:shadow-md transition-shadow overflow-hidden">
|
||||||
{/* Build Image */}
|
{/* Build Image */}
|
||||||
<div className="h-48 bg-gray-200 relative">
|
<div className="h-48 bg-gray-200 relative">
|
||||||
|
|||||||
417
src/app/my-builds/page.tsx
Normal file
417
src/app/my-builds/page.tsx
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import SearchInput from '@/components/SearchInput';
|
||||||
|
|
||||||
|
// Sample build data
|
||||||
|
const sampleMyBuilds = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Budget AR-15 Build',
|
||||||
|
description: 'A cost-effective AR-15 build using quality budget components',
|
||||||
|
status: 'completed' as const,
|
||||||
|
totalCost: 847.50,
|
||||||
|
completedDate: '2024-01-15',
|
||||||
|
components: {
|
||||||
|
total: 18,
|
||||||
|
completed: 18,
|
||||||
|
categories: {
|
||||||
|
'Upper': 8,
|
||||||
|
'Lower': 7,
|
||||||
|
'Accessory': 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags: ['Budget', '5.56 NATO', '16" Barrel'],
|
||||||
|
image: 'https://picsum.photos/400/250?random=1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Precision Long Range',
|
||||||
|
description: 'High-end precision build optimized for long-range accuracy',
|
||||||
|
status: 'in-progress' as const,
|
||||||
|
totalCost: 2847.99,
|
||||||
|
startedDate: '2024-02-01',
|
||||||
|
components: {
|
||||||
|
total: 18,
|
||||||
|
completed: 12,
|
||||||
|
categories: {
|
||||||
|
'Upper': 6,
|
||||||
|
'Lower': 4,
|
||||||
|
'Accessory': 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags: ['Precision', '6.5 Creedmoor', '20" Barrel'],
|
||||||
|
image: 'https://picsum.photos/400/250?random=2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: 'Home Defense Setup',
|
||||||
|
description: 'Compact AR-15 configured for home defense scenarios',
|
||||||
|
status: 'planning' as const,
|
||||||
|
totalCost: 0,
|
||||||
|
plannedDate: '2024-03-01',
|
||||||
|
components: {
|
||||||
|
total: 18,
|
||||||
|
completed: 0,
|
||||||
|
categories: {
|
||||||
|
'Upper': 0,
|
||||||
|
'Lower': 0,
|
||||||
|
'Accessory': 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags: ['Home Defense', '5.56 NATO', '10.5" Barrel'],
|
||||||
|
image: 'https://picsum.photos/400/250?random=3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: 'Competition Rifle',
|
||||||
|
description: 'Lightweight competition build for 3-gun matches',
|
||||||
|
status: 'completed' as const,
|
||||||
|
totalCost: 1650.75,
|
||||||
|
completedDate: '2023-12-10',
|
||||||
|
components: {
|
||||||
|
total: 18,
|
||||||
|
completed: 18,
|
||||||
|
categories: {
|
||||||
|
'Upper': 8,
|
||||||
|
'Lower': 7,
|
||||||
|
'Accessory': 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags: ['Competition', '5.56 NATO', '18" Barrel'],
|
||||||
|
image: 'https://picsum.photos/400/250?random=4'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
name: 'Suppressed SBR',
|
||||||
|
description: 'Short-barreled rifle build with suppressor integration',
|
||||||
|
status: 'in-progress' as const,
|
||||||
|
totalCost: 1895.25,
|
||||||
|
startedDate: '2024-01-20',
|
||||||
|
components: {
|
||||||
|
total: 18,
|
||||||
|
completed: 8,
|
||||||
|
categories: {
|
||||||
|
'Upper': 4,
|
||||||
|
'Lower': 3,
|
||||||
|
'Accessory': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags: ['SBR', 'Suppressed', '300 BLK'],
|
||||||
|
image: 'https://picsum.photos/400/250?random=5'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
name: 'Retro M16A1 Clone',
|
||||||
|
description: 'Faithful reproduction of the classic M16A1 rifle',
|
||||||
|
status: 'planning' as const,
|
||||||
|
totalCost: 0,
|
||||||
|
plannedDate: '2024-04-01',
|
||||||
|
components: {
|
||||||
|
total: 18,
|
||||||
|
completed: 0,
|
||||||
|
categories: {
|
||||||
|
'Upper': 0,
|
||||||
|
'Lower': 0,
|
||||||
|
'Accessory': 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags: ['Retro', '5.56 NATO', '20" Barrel'],
|
||||||
|
image: 'https://picsum.photos/400/250?random=6'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
type MyBuildStatus = 'completed' | 'in-progress' | 'planning';
|
||||||
|
type SortField = 'name' | 'status' | 'totalCost' | 'completedDate';
|
||||||
|
type SortDirection = 'asc' | 'desc';
|
||||||
|
|
||||||
|
export default function MyBuildsPage() {
|
||||||
|
const [sortField, setSortField] = useState<SortField>('completedDate');
|
||||||
|
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
|
||||||
|
const [selectedStatus, setSelectedStatus] = useState<MyBuildStatus | 'all'>('all');
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
// Filter builds
|
||||||
|
const filteredMyBuilds = sampleMyBuilds.filter(build => {
|
||||||
|
if (selectedStatus !== 'all' && build.status !== selectedStatus) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchTerm && !build.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
|
||||||
|
!build.description.toLowerCase().includes(searchTerm.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort builds
|
||||||
|
const sortedMyBuilds = [...filteredMyBuilds].sort((a, b) => {
|
||||||
|
let aValue: any, bValue: any;
|
||||||
|
|
||||||
|
if (sortField === 'totalCost') {
|
||||||
|
aValue = a.totalCost;
|
||||||
|
bValue = b.totalCost;
|
||||||
|
} else if (sortField === 'status') {
|
||||||
|
aValue = a.status;
|
||||||
|
bValue = b.status;
|
||||||
|
} else if (sortField === 'completedDate') {
|
||||||
|
aValue = a.completedDate || a.startedDate || a.plannedDate || '';
|
||||||
|
bValue = b.completedDate || b.startedDate || b.plannedDate || '';
|
||||||
|
} else {
|
||||||
|
aValue = a.name.toLowerCase();
|
||||||
|
bValue = b.name.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortDirection === 'asc') {
|
||||||
|
return aValue > bValue ? 1 : -1;
|
||||||
|
} else {
|
||||||
|
return aValue < bValue ? 1 : -1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const getStatusColor = (status: MyBuildStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed':
|
||||||
|
return 'bg-green-100 text-green-800';
|
||||||
|
case 'in-progress':
|
||||||
|
return 'bg-blue-100 text-blue-800';
|
||||||
|
case 'planning':
|
||||||
|
return 'bg-yellow-100 text-yellow-800';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusIcon = (status: MyBuildStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed':
|
||||||
|
return '✓';
|
||||||
|
case 'in-progress':
|
||||||
|
return '🔄';
|
||||||
|
case 'planning':
|
||||||
|
return '📋';
|
||||||
|
default:
|
||||||
|
return '❓';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProgressPercentage = (build: typeof sampleMyBuilds[0]) => {
|
||||||
|
return Math.round((build.components.completed / build.components.total) * 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalMyBuilds = sampleMyBuilds.length;
|
||||||
|
const completedMyBuilds = sampleMyBuilds.filter(build => build.status === 'completed').length;
|
||||||
|
const inProgressMyBuilds = sampleMyBuilds.filter(build => build.status === 'in-progress').length;
|
||||||
|
const totalValue = sampleMyBuilds.reduce((sum, build) => sum + build.totalCost, 0);
|
||||||
|
|
||||||
|
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">My Builds</h1>
|
||||||
|
<p className="text-gray-600 mt-2">Track and manage your firearm builds</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">{totalMyBuilds}</div>
|
||||||
|
<div className="text-sm text-gray-500">Total Builds</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-green-600">{completedMyBuilds}</div>
|
||||||
|
<div className="text-sm text-gray-500">Completed</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-blue-600">{inProgressMyBuilds}</div>
|
||||||
|
<div className="text-sm text-gray-500">In Progress</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-purple-600">${totalValue.toFixed(2)}</div>
|
||||||
|
<div className="text-sm text-gray-500">Total Value</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">
|
||||||
|
{/* Filters Row */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
{/* Status Filter */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
||||||
|
<select
|
||||||
|
value={selectedStatus}
|
||||||
|
onChange={(e) => setSelectedStatus(e.target.value as MyBuildStatus | 'all')}
|
||||||
|
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="completed">Completed</option>
|
||||||
|
<option value="in-progress">In Progress</option>
|
||||||
|
<option value="planning">Planning</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) => setSortField(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="completedDate">Date</option>
|
||||||
|
<option value="name">Name</option>
|
||||||
|
<option value="totalCost">Cost</option>
|
||||||
|
<option value="status">Status</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sort Direction */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Order</label>
|
||||||
|
<select
|
||||||
|
value={sortDirection}
|
||||||
|
onChange={(e) => setSortDirection(e.target.value as SortDirection)}
|
||||||
|
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="desc">Newest First</option>
|
||||||
|
<option value="asc">Oldest First</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* New Build Button */}
|
||||||
|
<div className="flex items-end">
|
||||||
|
<button className="w-full bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">
|
||||||
|
+ New Build
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Builds Grid */}
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
{sortedMyBuilds.length > 0 ? (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{sortedMyBuilds.map((build) => (
|
||||||
|
<div key={build.id} className="bg-white rounded-lg shadow-sm border hover:shadow-md transition-shadow overflow-hidden">
|
||||||
|
{/* Build Image */}
|
||||||
|
<div className="h-48 bg-gray-200 relative">
|
||||||
|
<img
|
||||||
|
src={build.image}
|
||||||
|
alt={build.name}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
target.style.display = 'none';
|
||||||
|
target.nextElementSibling?.classList.remove('hidden');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="hidden w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-300 to-gray-400">
|
||||||
|
<div className="text-center text-gray-600">
|
||||||
|
<div className="text-4xl mb-2">🔫</div>
|
||||||
|
<div className="text-sm font-medium">{build.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-3 right-3">
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(build.status)}`}>
|
||||||
|
{getStatusIcon(build.status)} {build.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Build Content */}
|
||||||
|
<div className="p-6">
|
||||||
|
{/* Build Title and Date */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-1">{build.name}</h3>
|
||||||
|
<p className="text-sm text-gray-600 mb-2">{build.description}</p>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{build.status === 'completed' && build.completedDate && `Completed ${formatDate(build.completedDate)}`}
|
||||||
|
{build.status === 'in-progress' && build.startedDate && `Started ${formatDate(build.startedDate)}`}
|
||||||
|
{build.status === 'planning' && build.plannedDate && `Planned for ${formatDate(build.plannedDate)}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex justify-between text-sm text-gray-600 mb-1">
|
||||||
|
<span>Progress</span>
|
||||||
|
<span>{build.components.completed}/{build.components.total} components</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||||
|
style={{ width: `${getProgressPercentage(build)}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Component Categories */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
{Object.entries(build.components.categories).map(([category, count]) => (
|
||||||
|
<span key={category} className="inline-flex items-center px-2 py-1 rounded text-xs bg-gray-100 text-gray-700">
|
||||||
|
{category}: {count}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tags */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{build.tags.map((tag) => (
|
||||||
|
<span key={tag} className="inline-flex items-center px-2 py-1 rounded text-xs bg-blue-100 text-blue-800">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cost and Actions */}
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="text-lg font-bold text-gray-900">
|
||||||
|
${build.totalCost.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button className="bg-blue-600 text-white px-3 py-1 rounded text-sm hover:bg-blue-700 transition-colors">
|
||||||
|
View Details
|
||||||
|
</button>
|
||||||
|
<button className="bg-gray-100 text-gray-700 px-2 py-1 rounded text-sm hover:bg-gray-200 transition-colors">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="text-gray-500">
|
||||||
|
<div className="text-lg font-medium mb-2">No builds found</div>
|
||||||
|
<div className="text-sm">Try adjusting your filters or create a new build</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { Listbox, Transition } from '@headlessui/react';
|
import { Listbox, Transition } from '@headlessui/react';
|
||||||
import { ChevronUpDownIcon, CheckIcon, XMarkIcon } from '@heroicons/react/20/solid';
|
import { ChevronUpDownIcon, CheckIcon, XMarkIcon, TableCellsIcon, Squares2X2Icon } from '@heroicons/react/20/solid';
|
||||||
import SearchInput from '@/components/SearchInput';
|
import SearchInput from '@/components/SearchInput';
|
||||||
import ProductCard from '@/components/ProductCard';
|
import ProductCard from '@/components/ProductCard';
|
||||||
import RestrictionAlert from '@/components/RestrictionAlert';
|
import RestrictionAlert from '@/components/RestrictionAlert';
|
||||||
@@ -11,6 +11,7 @@ import Tooltip from '@/components/Tooltip';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { mockProducts } from '@/mock/product';
|
import { mockProducts } from '@/mock/product';
|
||||||
import type { Product } from '@/mock/product';
|
import type { Product } from '@/mock/product';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
// Extract unique values for dropdowns
|
// Extract unique values for dropdowns
|
||||||
const categories = ['All', ...Array.from(new Set(mockProducts.map(part => part.category.name)))];
|
const categories = ['All', ...Array.from(new Set(mockProducts.map(part => part.category.name)))];
|
||||||
@@ -464,14 +465,16 @@ export default function Home() {
|
|||||||
<button
|
<button
|
||||||
className={`btn btn-sm ${viewMode === 'table' ? 'btn-active' : ''}`}
|
className={`btn btn-sm ${viewMode === 'table' ? 'btn-active' : ''}`}
|
||||||
onClick={() => setViewMode('table')}
|
onClick={() => setViewMode('table')}
|
||||||
|
aria-label="Table view"
|
||||||
>
|
>
|
||||||
Table
|
<TableCellsIcon className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm ${viewMode === 'cards' ? 'btn-active' : ''}`}
|
className={`btn btn-sm ${viewMode === 'cards' ? 'btn-active' : ''}`}
|
||||||
onClick={() => setViewMode('cards')}
|
onClick={() => setViewMode('cards')}
|
||||||
|
aria-label="Card view"
|
||||||
>
|
>
|
||||||
Cards
|
<Squares2X2Icon className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -496,24 +499,12 @@ export default function Home() {
|
|||||||
<table className="min-w-full divide-y divide-neutral-200 dark:divide-neutral-700">
|
<table className="min-w-full divide-y divide-neutral-200 dark:divide-neutral-700">
|
||||||
<thead className="bg-neutral-50 dark:bg-neutral-700">
|
<thead className="bg-neutral-50 dark:bg-neutral-700">
|
||||||
<tr>
|
<tr>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
|
||||||
|
Product
|
||||||
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
|
||||||
Category
|
Category
|
||||||
</th>
|
</th>
|
||||||
<th
|
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-600"
|
|
||||||
onClick={() => handleSort('name')}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<span>Name</span>
|
|
||||||
<span className="text-sm">{getSortIcon('name')}</span>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
|
|
||||||
Brand
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
|
|
||||||
Description
|
|
||||||
</th>
|
|
||||||
<th
|
<th
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-600"
|
className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-600"
|
||||||
onClick={() => handleSort('price')}
|
onClick={() => handleSort('price')}
|
||||||
@@ -531,29 +522,20 @@ export default function Home() {
|
|||||||
<tbody className="bg-white dark:bg-neutral-800 divide-y divide-neutral-200 dark:divide-neutral-700">
|
<tbody className="bg-white dark:bg-neutral-800 divide-y divide-neutral-200 dark:divide-neutral-700">
|
||||||
{sortedParts.map((part) => (
|
{sortedParts.map((part) => (
|
||||||
<tr key={part.id} className="hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-colors">
|
<tr key={part.id} className="hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-colors">
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap flex items-center gap-3 min-w-[180px]">
|
||||||
|
<div className="w-12 h-12 flex-shrink-0 rounded bg-neutral-100 dark:bg-neutral-700 overflow-hidden flex items-center justify-center border border-neutral-200 dark:border-neutral-700">
|
||||||
|
<Image src={Array.isArray(part.images) && (part.images as string[]).length > 0 ? (part.images as string[])[0] : '/window.svg'} alt={part.name} width={48} height={48} className="object-contain w-12 h-12" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-semibold text-neutral-900 dark:text-white">{part.name}</div>
|
||||||
|
<div className="text-xs text-neutral-500 dark:text-neutral-400">{part.brand.name}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<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-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200">
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200">
|
||||||
{part.category.name}
|
{part.category.name}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="text-sm font-medium text-neutral-900 dark:text-white">
|
|
||||||
{part.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-neutral-500 dark:text-neutral-400">
|
|
||||||
{part.brand.name}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="text-sm text-neutral-900 dark:text-white">
|
|
||||||
{part.brand.name}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<div className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
|
||||||
{part.description}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm font-semibold text-neutral-900 dark:text-white">
|
<div className="text-sm font-semibold text-neutral-900 dark:text-white">
|
||||||
${Math.min(...part.offers.map(offer => offer.price)).toFixed(2)}
|
${Math.min(...part.offers.map(offer => offer.price)).toFixed(2)}
|
||||||
@@ -562,7 +544,7 @@ export default function Home() {
|
|||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
<Link href={`/products/${part.id}`} legacyBehavior>
|
<Link href={`/products/${part.id}`} legacyBehavior>
|
||||||
<a className="btn btn-primary btn-sm">
|
<a className="btn btn-primary btn-sm">
|
||||||
View Details
|
Add
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ export default function Navbar() {
|
|||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ href: '/parts', label: 'Parts Catalog' },
|
{ href: '/parts', label: 'Parts Catalog' },
|
||||||
{ href: '/build', label: 'Build Checklist' },
|
{ href: '/build', label: 'Plan a Build' },
|
||||||
{ href: '/builds', label: 'My Builds' },
|
{ href: '/my-builds', label: 'My Builds' },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user