Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f43f68709a | |||
| 8015a78357 | |||
| 926df49f4c | |||
| b2ada8d81e |
@@ -17,7 +17,7 @@ export default function ForgotPasswordPage() {
|
||||
<h1 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">Forgot your password?</h1>
|
||||
<p className="mb-6 text-gray-600 dark:text-gray-300 text-sm">
|
||||
Enter your email address and we'll send you a link to reset your password.<br/>
|
||||
<span className="text-primary font-semibold">(This feature is not yet implemented.)</span>
|
||||
<span className="text-blue-600 font-semibold">(This feature is not yet implemented.)</span>
|
||||
</p>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<input
|
||||
@@ -31,14 +31,14 @@ export default function ForgotPasswordPage() {
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full btn btn-primary"
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium text-sm py-2 px-4 rounded-md transition-colors"
|
||||
disabled={submitted}
|
||||
>
|
||||
{submitted ? 'Check your email' : 'Send reset link'}
|
||||
</button>
|
||||
</form>
|
||||
<div className="mt-6 text-center">
|
||||
<Link href="/account/login" className="text-primary-600 hover:underline text-sm">Back to login</Link>
|
||||
<Link href="/account/login" className="text-blue-600 hover:underline text-sm">Back to login</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function LoginPage() {
|
||||
<h2 className="mt-6 text-3xl font-extrabold text-gray-900 dark:text-white">Sign in to your account</h2>
|
||||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
Or{' '}
|
||||
<Link href="/account/register" className="font-medium text-primary-600 hover:text-primary-500">
|
||||
<Link href="/account/register" className="font-medium text-blue-600 hover:text-blue-500">
|
||||
Sign Up For Free
|
||||
</Link>
|
||||
</p>
|
||||
@@ -75,7 +75,7 @@ export default function LoginPage() {
|
||||
required
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-zinc-700 placeholder-gray-500 dark:placeholder-zinc-400 text-gray-900 dark:text-white bg-white dark:bg-zinc-800 rounded-t-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-zinc-700 placeholder-gray-500 dark:placeholder-zinc-400 text-gray-900 dark:text-white bg-white dark:bg-zinc-800 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Email address"
|
||||
disabled={loading}
|
||||
/>
|
||||
@@ -92,7 +92,7 @@ export default function LoginPage() {
|
||||
required
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-zinc-700 placeholder-gray-500 dark:placeholder-zinc-400 text-gray-900 dark:text-white bg-white dark:bg-zinc-800 rounded-b-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-zinc-700 placeholder-gray-500 dark:placeholder-zinc-400 text-gray-900 dark:text-white bg-white dark:bg-zinc-800 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Password"
|
||||
disabled={loading}
|
||||
/>
|
||||
@@ -109,7 +109,7 @@ export default function LoginPage() {
|
||||
id="remember-me"
|
||||
name="remember-me"
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-primary"
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
disabled={loading}
|
||||
/>
|
||||
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900 dark:text-gray-300">
|
||||
@@ -118,7 +118,7 @@ export default function LoginPage() {
|
||||
</div>
|
||||
|
||||
<div className="text-sm">
|
||||
<Link href="/account/forgot-password" className="font-medium text-primary-600 hover:text-primary-500">
|
||||
<Link href="/account/forgot-password" className="font-medium text-blue-600 hover:text-blue-500">
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</div>
|
||||
@@ -127,7 +127,7 @@ export default function LoginPage() {
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full btn btn-primary text-white font-medium text-sm py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium text-sm py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Signing in...' : 'Sign in'}
|
||||
@@ -151,7 +151,7 @@ export default function LoginPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGoogle}
|
||||
className="btn btn-outline flex justify-center items-center py-2 px-4 border border-gray-300 dark:border-zinc-700 rounded-md shadow-sm bg-white dark:bg-zinc-800 text-sm font-medium text-gray-500 dark:text-zinc-300 hover:bg-gray-50 dark:hover:bg-zinc-700 transition"
|
||||
className="flex justify-center items-center py-2 px-4 border border-gray-300 dark:border-zinc-700 rounded-md shadow-sm bg-white dark:bg-zinc-800 text-sm font-medium text-gray-500 dark:text-zinc-300 hover:bg-gray-50 dark:hover:bg-zinc-700 transition"
|
||||
disabled={loading}
|
||||
>
|
||||
<span className="sr-only">Sign in with Google</span>
|
||||
|
||||
@@ -79,14 +79,14 @@ export default function RegisterPage() {
|
||||
{error && <div className="text-red-600 text-sm">{error}</div>}
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full btn btn-primary text-white font-medium text-sm py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium text-sm py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Creating account...' : 'Create Account'}
|
||||
</button>
|
||||
</form>
|
||||
<div className="mt-6 text-center">
|
||||
<Link href="/account/login" className="text-primary-600 hover:underline text-sm">Already have an account? Sign in</Link>
|
||||
<Link href="/account/login" className="text-blue-600 hover:underline text-sm">Already have an account? Sign in</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -408,7 +408,7 @@ export default function BuildPage() {
|
||||
<div className="text-sm text-gray-500">Total Cost</div>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-outline btn-error ml-0 md:ml-4"
|
||||
className="border border-red-300 hover:bg-red-50 text-red-700 ml-0 md:ml-4 px-4 py-2 rounded-md transition-colors"
|
||||
onClick={() => setShowClearModal(true)}
|
||||
>
|
||||
Clear Build
|
||||
@@ -429,13 +429,13 @@ export default function BuildPage() {
|
||||
</Dialog.Description>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
className="btn btn-sm btn-ghost"
|
||||
className="text-gray-700 hover:bg-gray-100 px-3 py-1 rounded-md text-sm transition-colors"
|
||||
onClick={() => setShowClearModal(false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-error"
|
||||
className="bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded-md text-sm transition-colors"
|
||||
onClick={() => {
|
||||
clearBuild();
|
||||
setShowClearModal(false);
|
||||
@@ -717,7 +717,7 @@ export default function BuildPage() {
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
{selected ? (
|
||||
<button
|
||||
className="btn btn-outline btn-sm"
|
||||
className="border border-gray-300 hover:bg-gray-50 text-gray-700 px-3 py-1 rounded-md text-sm font-medium transition-colors"
|
||||
onClick={() => removePartForComponent(component.id)}
|
||||
>
|
||||
Remove
|
||||
@@ -725,7 +725,7 @@ export default function BuildPage() {
|
||||
) : (
|
||||
<Link
|
||||
href={`/parts?category=${encodeURIComponent(getProductCategoryForComponent(component.name))}`}
|
||||
className="btn btn-primary btn-sm"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded-md text-sm font-medium transition-colors"
|
||||
>
|
||||
Find Parts
|
||||
</Link>
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function LandingPage() {
|
||||
<div className="mt-10 flex items-top gap-x-6">
|
||||
<Link
|
||||
href="/build"
|
||||
className="btn btn-primary text-base font-semibold px-6"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white text-base font-semibold px-6 py-3 rounded-md transition-colors"
|
||||
>
|
||||
Get Building
|
||||
</Link>
|
||||
|
||||
+165
-29
@@ -159,7 +159,7 @@ const Dropdown = ({
|
||||
key={optionIdx}
|
||||
className={({ active }) =>
|
||||
`relative cursor-default select-none py-2 pl-10 pr-4 ${
|
||||
active ? 'bg-primary-100 text-primary-900' : 'text-zinc-900'
|
||||
active ? 'bg-blue-100 text-blue-900' : 'text-zinc-900'
|
||||
}`
|
||||
}
|
||||
value={option.value}
|
||||
@@ -227,6 +227,123 @@ const getMatchingComponentName = (productCategory: string): string => {
|
||||
return categoryToComponentType[productCategory] || '';
|
||||
};
|
||||
|
||||
// Pagination Component
|
||||
const Pagination = ({
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
itemsPerPage,
|
||||
onItemsPerPageChange,
|
||||
totalItems,
|
||||
startIndex,
|
||||
endIndex
|
||||
}: {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
itemsPerPage: number;
|
||||
onItemsPerPageChange: (items: number) => void;
|
||||
totalItems: number;
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
}) => {
|
||||
const getPageNumbers = () => {
|
||||
const pages = [];
|
||||
const maxVisiblePages = 5;
|
||||
|
||||
if (totalPages <= maxVisiblePages) {
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
if (currentPage <= 3) {
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
pages.push('...');
|
||||
pages.push(totalPages);
|
||||
} else if (currentPage >= totalPages - 2) {
|
||||
pages.push(1);
|
||||
pages.push('...');
|
||||
for (let i = totalPages - 3; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
pages.push(1);
|
||||
pages.push('...');
|
||||
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
pages.push('...');
|
||||
pages.push(totalPages);
|
||||
}
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row justify-between items-center gap-4 py-4 px-6 bg-white border-t border-zinc-200">
|
||||
{/* Items per page selector */}
|
||||
<div className="flex items-center gap-2 text-sm text-zinc-600">
|
||||
<span>Show:</span>
|
||||
<select
|
||||
value={itemsPerPage}
|
||||
onChange={(e) => onItemsPerPageChange(Number(e.target.value))}
|
||||
className="border border-zinc-300 rounded px-2 py-1 text-sm"
|
||||
>
|
||||
<option value={10}>10</option>
|
||||
<option value={20}>20</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
</select>
|
||||
<span>per page</span>
|
||||
</div>
|
||||
|
||||
{/* Page info */}
|
||||
<div className="text-sm text-zinc-600">
|
||||
Showing {startIndex + 1} to {Math.min(endIndex, totalItems)} of {totalItems} results
|
||||
</div>
|
||||
|
||||
{/* Pagination controls */}
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 py-1 text-sm border border-zinc-300 rounded hover:bg-zinc-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
|
||||
{getPageNumbers().map((page, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => typeof page === 'number' ? onPageChange(page) : null}
|
||||
disabled={page === '...'}
|
||||
className={`px-3 py-1 text-sm border rounded ${
|
||||
page === currentPage
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: page === '...'
|
||||
? 'border-zinc-300 text-zinc-400 cursor-default'
|
||||
: 'border-zinc-300 hover:bg-zinc-50'
|
||||
}`}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-3 py-1 text-sm border border-zinc-300 rounded hover:bg-zinc-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
@@ -247,6 +364,10 @@ export default function Home() {
|
||||
const [viewMode, setViewMode] = useState<'table' | 'cards'>('table');
|
||||
const [addedPartIds, setAddedPartIds] = useState<string[]>([]);
|
||||
const [isSearchExpanded, setIsSearchExpanded] = useState(false);
|
||||
|
||||
// Pagination state
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(20);
|
||||
const selectPartForComponent = useBuildStore((state) => state.selectPartForComponent);
|
||||
const selectedParts = useBuildStore((state) => state.selectedParts);
|
||||
const removePartForComponent = useBuildStore((state) => state.removePartForComponent);
|
||||
@@ -386,6 +507,17 @@ export default function Home() {
|
||||
}
|
||||
});
|
||||
|
||||
// Pagination logic
|
||||
const totalPages = Math.ceil(sortedParts.length / itemsPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const paginatedParts = sortedParts.slice(startIndex, endIndex);
|
||||
|
||||
// Reset to first page when filters change
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [selectedCategoryId, selectedSubcategoryId, selectedBrand, selectedVendor, searchTerm, priceRange, selectedRestriction, sortField, sortDirection]);
|
||||
|
||||
const handleSort = (field: SortField) => {
|
||||
if (sortField === field) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
@@ -588,7 +720,7 @@ export default function Home() {
|
||||
key={optionIdx}
|
||||
className={({ active }) =>
|
||||
`relative cursor-default select-none py-2 pl-10 pr-4 ${
|
||||
active ? 'bg-primary-100 text-primary-900' : 'text-zinc-900'
|
||||
active ? 'bg-blue-100 text-blue-900' : 'text-zinc-900'
|
||||
}`
|
||||
}
|
||||
value={option.value}
|
||||
@@ -599,7 +731,7 @@ export default function Home() {
|
||||
{option.label}
|
||||
</span>
|
||||
{selected ? (
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-primary-600">
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-blue-600">
|
||||
<CheckIcon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
@@ -648,11 +780,11 @@ export default function Home() {
|
||||
{/* View Toggle and Results Count */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div className="text-sm text-zinc-700">
|
||||
{loading ? 'Loading...' : `Showing ${sortedParts.length} of ${products.length} parts`}
|
||||
{loading ? 'Loading...' : `Showing ${startIndex + 1}-${Math.min(endIndex, sortedParts.length)} of ${sortedParts.length} parts`}
|
||||
{hasActiveFilters && !loading && (
|
||||
<span className="ml-2 text-primary-600">
|
||||
(filtered)
|
||||
</span>
|
||||
<span className="ml-2 text-blue-600">
|
||||
(filtered)
|
||||
</span>
|
||||
)}
|
||||
{error && <span className="ml-2 text-red-500">{error}</span>}
|
||||
</div>
|
||||
@@ -660,16 +792,16 @@ export default function Home() {
|
||||
{/* View Toggle */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-zinc-600">View:</span>
|
||||
<div className="btn-group">
|
||||
<div className="flex">
|
||||
<button
|
||||
className={`btn btn-sm ${viewMode === 'table' ? 'btn-active' : ''}`}
|
||||
className={`px-3 py-1 text-sm border rounded-l ${viewMode === 'table' ? 'bg-blue-600 text-white border-blue-600' : 'border-gray-300 hover:bg-gray-50'}`}
|
||||
onClick={() => setViewMode('table')}
|
||||
aria-label="Table view"
|
||||
>
|
||||
<TableCellsIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-sm ${viewMode === 'cards' ? 'btn-active' : ''}`}
|
||||
className={`px-3 py-1 text-sm border border-l-0 rounded-r ${viewMode === 'cards' ? 'bg-blue-600 text-white border-blue-600' : 'border-gray-300 hover:bg-gray-50'}`}
|
||||
onClick={() => setViewMode('cards')}
|
||||
aria-label="Card view"
|
||||
>
|
||||
@@ -707,20 +839,20 @@ export default function Home() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-zinc-200">
|
||||
{sortedParts.map((part) => (
|
||||
{paginatedParts.map((part) => (
|
||||
<tr key={part.id} className="hover:bg-zinc-50 transition-colors">
|
||||
<td className="px-0 py-2 flex items-center gap-2 align-top">
|
||||
<div className="w-12 h-12 flex-shrink-0 rounded bg-zinc-100 overflow-hidden flex items-center justify-center border border-zinc-200">
|
||||
<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 className="max-w-md break-words whitespace-normal">
|
||||
<Link href={`/products/${part.slug}`} className="text-sm font-semibold text-primary hover:underline">
|
||||
<Link href={`/products/${part.slug}`} className="text-sm font-semibold text-blue-600 hover:underline">
|
||||
{part.name}
|
||||
</Link>
|
||||
</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-primary-100 text-primary-800">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
{part.category.name}
|
||||
</span>
|
||||
</td>
|
||||
@@ -743,7 +875,7 @@ export default function Home() {
|
||||
if (selectedComponentId) {
|
||||
return (
|
||||
<button
|
||||
className="btn btn-outline btn-sm"
|
||||
className="border border-gray-300 hover:bg-gray-50 text-gray-700 px-3 py-1 rounded-md text-sm font-medium transition-colors"
|
||||
onClick={() => removePartForComponent(selectedComponentId)}
|
||||
>
|
||||
Remove
|
||||
@@ -752,7 +884,7 @@ export default function Home() {
|
||||
} else if (matchingComponent && !selectedParts[matchingComponent.id]) {
|
||||
return (
|
||||
<button
|
||||
className="btn btn-neutral btn-sm flex items-center gap-1"
|
||||
className="bg-gray-600 hover:bg-gray-700 text-white px-3 py-1 rounded-md text-sm font-medium transition-colors flex items-center gap-1"
|
||||
onClick={() => {
|
||||
selectPartForComponent(matchingComponent.id, {
|
||||
id: part.id,
|
||||
@@ -782,26 +914,30 @@ export default function Home() {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Table Footer */}
|
||||
<div className="bg-zinc-50 px-6 py-3 border-t border-zinc-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-zinc-700">
|
||||
Showing {sortedParts.length} of {products.length} parts
|
||||
{hasActiveFilters && (
|
||||
<span className="ml-2 text-primary-600">
|
||||
(filtered)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={setCurrentPage}
|
||||
itemsPerPage={itemsPerPage}
|
||||
onItemsPerPageChange={(items) => {
|
||||
setItemsPerPage(items);
|
||||
setCurrentPage(1); // Reset to first page when changing items per page
|
||||
}}
|
||||
totalItems={sortedParts.length}
|
||||
startIndex={startIndex}
|
||||
endIndex={endIndex}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Card View */}
|
||||
{viewMode === 'cards' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{sortedParts.map((part) => (
|
||||
{paginatedParts.map((part) => (
|
||||
<ProductCard key={part.id} product={part} onAdd={() => handleAdd(part)} added={addedPartIds.includes(part.id)} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -213,7 +213,7 @@ export default function ProductDetailsPage() {
|
||||
|
||||
{/* Price Range */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-3xl font-bold text-primary-600">
|
||||
<div className="text-3xl font-bold text-blue-600">
|
||||
${lowestPrice.toFixed(2)}
|
||||
</div>
|
||||
{lowestPrice !== highestPrice && (
|
||||
@@ -236,10 +236,10 @@ export default function ProductDetailsPage() {
|
||||
|
||||
{/* Add to Build Button */}
|
||||
<div className="flex gap-4">
|
||||
<button className="btn btn-primary flex-1" onClick={handleAddToBuild}>
|
||||
<button className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors flex-1" onClick={handleAddToBuild}>
|
||||
Add to Current Build
|
||||
</button>
|
||||
<button className="btn btn-outline">
|
||||
<button className="border border-gray-300 hover:bg-gray-50 text-gray-700 font-medium py-2 px-4 rounded-md transition-colors">
|
||||
Save for Later
|
||||
</button>
|
||||
</div>
|
||||
@@ -278,7 +278,7 @@ export default function ProductDetailsPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-primary-600">
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
${offer.price.toFixed(2)}
|
||||
</div>
|
||||
{offer.inStock !== undefined && (
|
||||
@@ -291,7 +291,7 @@ export default function ProductDetailsPage() {
|
||||
href={offer.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-primary"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
|
||||
>
|
||||
View Deal
|
||||
</a>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
import { useState, ReactNode } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogBackdrop,
|
||||
@@ -32,8 +33,8 @@ const navigation = [
|
||||
// { name: 'Settings', href: '/admin/settings', icon: Cog6ToothIcon }, // optional/future
|
||||
];
|
||||
const userNavigation = [
|
||||
{ name: 'Your profile', href: '#' },
|
||||
{ name: 'Sign out', href: '#' },
|
||||
{ name: 'Your profile', href: '/account/profile' },
|
||||
{ name: 'Sign out', href: '/api/auth/signout' },
|
||||
];
|
||||
|
||||
function classNames(...classes: string[]) {
|
||||
@@ -43,6 +44,40 @@ function classNames(...classes: string[]) {
|
||||
export default function AdminNavbar({ children }: { children: ReactNode }) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const { data: session } = useSession();
|
||||
|
||||
// Get user display name
|
||||
const getUserDisplayName = () => {
|
||||
if (!session?.user) return 'Admin User';
|
||||
|
||||
const user = session.user as any;
|
||||
if (user.first_name && user.last_name) {
|
||||
return `${user.first_name} ${user.last_name}`;
|
||||
}
|
||||
if (user.name) {
|
||||
return user.name;
|
||||
}
|
||||
if (user.email) {
|
||||
return user.email.split('@')[0]; // Use email prefix as fallback
|
||||
}
|
||||
return 'Admin User';
|
||||
};
|
||||
|
||||
const getUserInitials = () => {
|
||||
if (!session?.user) return 'A';
|
||||
|
||||
const user = session.user as any;
|
||||
if (user.first_name && user.last_name) {
|
||||
return `${user.first_name[0]}${user.last_name[0]}`.toUpperCase();
|
||||
}
|
||||
if (user.name) {
|
||||
return user.name.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2);
|
||||
}
|
||||
if (user.email) {
|
||||
return user.email[0].toUpperCase();
|
||||
}
|
||||
return 'A';
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -203,6 +238,15 @@ export default function AdminNavbar({ children }: { children: ReactNode }) {
|
||||
className="pointer-events-none col-start-1 row-start-1 size-5 self-center text-gray-400"
|
||||
/>
|
||||
</form>
|
||||
|
||||
{/* Back to Site Button */}
|
||||
<a
|
||||
href="/"
|
||||
className="inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
<HomeIcon className="w-4 h-4 mr-2" />
|
||||
Back to Site
|
||||
</a>
|
||||
<div className="flex items-center gap-x-4 lg:gap-x-6">
|
||||
<button type="button" className="-m-2.5 p-2.5 text-gray-400 hover:text-gray-500">
|
||||
<span className="sr-only">View notifications</span>
|
||||
@@ -217,27 +261,32 @@ export default function AdminNavbar({ children }: { children: ReactNode }) {
|
||||
<MenuButton className="relative flex items-center">
|
||||
<span className="absolute -inset-1.5" />
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<img
|
||||
alt=""
|
||||
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
className="size-8 rounded-full bg-gray-50"
|
||||
/>
|
||||
<div className="size-8 rounded-full bg-indigo-600 flex items-center justify-center">
|
||||
<span className="text-sm font-medium text-white">
|
||||
{getUserInitials()}
|
||||
</span>
|
||||
</div>
|
||||
<span className="hidden lg:flex lg:items-center">
|
||||
<span aria-hidden="true" className="ml-4 text-sm/6 font-semibold text-gray-900">
|
||||
Tom Cook
|
||||
{getUserDisplayName()}
|
||||
</span>
|
||||
<ChevronDownIcon aria-hidden="true" className="ml-2 size-5 text-gray-400" />
|
||||
</span>
|
||||
</MenuButton>
|
||||
<MenuItems
|
||||
transition
|
||||
className="absolute right-0 z-10 mt-2.5 w-32 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 transition focus:outline-none data-[closed]:scale-95 data-[closed]:transform data-[closed]:opacity-0 data-[enter]:duration-100 data-[leave]:duration-75 data-[enter]:ease-out data-[leave]:ease-in"
|
||||
className="absolute right-0 z-10 mt-2.5 w-48 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 transition focus:outline-none data-[closed]:scale-95 data-[closed]:transform data-[closed]:opacity-0 data-[enter]:duration-100 data-[leave]:duration-75 data-[enter]:ease-out data-[leave]:ease-in"
|
||||
>
|
||||
{session?.user && (
|
||||
<div className="px-3 py-2 text-xs text-gray-500 border-b border-gray-100">
|
||||
{session.user.email}
|
||||
</div>
|
||||
)}
|
||||
{userNavigation.map((item) => (
|
||||
<MenuItem key={item.name}>
|
||||
<a
|
||||
href={item.href}
|
||||
className="block px-3 py-1 text-sm/6 text-gray-900 data-[focus]:bg-gray-50 data-[focus]:outline-none"
|
||||
className="block px-3 py-2 text-sm text-gray-900 data-[focus]:bg-gray-50 data-[focus]:outline-none hover:bg-gray-50"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { SessionProvider } from 'next-auth/react';
|
||||
|
||||
export default function AdminProviders({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<SessionProvider>
|
||||
{children}
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import AdminNavbar from './AdminNavbar';
|
||||
import AdminProviders from './AdminProviders';
|
||||
|
||||
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<AdminNavbar>
|
||||
{children}
|
||||
</AdminNavbar>
|
||||
<AdminProviders>
|
||||
<AdminNavbar>
|
||||
{children}
|
||||
</AdminNavbar>
|
||||
</AdminProviders>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import ThemeSwitcher from './ThemeSwitcher';
|
||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
||||
import { useSession, signIn, signOut } from 'next-auth/react';
|
||||
import { useState, ReactNode } from 'react';
|
||||
import { ShieldCheckIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
interface MenuItem {
|
||||
label: string;
|
||||
@@ -37,6 +38,14 @@ export default function Navbar() {
|
||||
onClick: () => setMenuOpen(false),
|
||||
}
|
||||
: undefined,
|
||||
// Admin link for admin users
|
||||
session?.user && (session.user as any)?.isAdmin
|
||||
? {
|
||||
label: 'Admin Dashboard',
|
||||
href: '/admin',
|
||||
onClick: () => setMenuOpen(false),
|
||||
}
|
||||
: undefined,
|
||||
session?.user
|
||||
? {
|
||||
label: 'Sign Out',
|
||||
@@ -64,6 +73,24 @@ export default function Navbar() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Admin Banner - Moved to top */}
|
||||
{session?.user && (session.user as any)?.isAdmin && (
|
||||
<div className="w-full bg-gradient-to-r from-purple-600 to-indigo-600 text-white py-2 px-4 sm:px-8 relative z-30">
|
||||
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<ShieldCheckIcon className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">Admin Panel</span>
|
||||
</div>
|
||||
<Link
|
||||
href="/admin"
|
||||
className="text-sm hover:text-purple-200 transition-colors underline underline-offset-2"
|
||||
>
|
||||
Go to Admin Dashboard →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Top Bar */}
|
||||
<div className="w-full bg-[#4B6516] text-white h-10 flex items-center justify-between px-4 sm:px-8 relative z-20">
|
||||
<Link href="/" className="font-bold text-lg tracking-tight hover:underline focus:underline">Pew Builder</Link>
|
||||
@@ -93,8 +120,10 @@ export default function Navbar() {
|
||||
{session.user.email}
|
||||
</div>
|
||||
)}
|
||||
{menuItems.map((item, idx) =>
|
||||
item.custom ? (
|
||||
{menuItems.map((item, idx) => {
|
||||
if (!item) return null;
|
||||
|
||||
return item.custom ? (
|
||||
<div key={idx} className="px-4 py-2 flex items-center justify-between">
|
||||
<span className="text-sm text-neutral-700 dark:text-neutral-200">Theme</span>
|
||||
{item.custom}
|
||||
@@ -105,7 +134,7 @@ export default function Navbar() {
|
||||
href={item.href}
|
||||
className={`block px-4 py-2 text-sm rounded transition-colors ${
|
||||
item.active
|
||||
? 'bg-primary/10 text-primary font-semibold'
|
||||
? 'bg-blue-100 text-blue-700 font-semibold'
|
||||
: 'text-neutral-700 dark:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-700'
|
||||
}`}
|
||||
onClick={item.onClick}
|
||||
@@ -120,15 +149,15 @@ export default function Navbar() {
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-sm btn-primary font-semibold px-4 py-1"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-semibold px-4 py-1 rounded-md text-sm transition-colors"
|
||||
onClick={() => signIn()}
|
||||
>
|
||||
Sign In
|
||||
@@ -148,8 +177,8 @@ export default function Navbar() {
|
||||
href={item.href}
|
||||
className={`px-2 py-1 rounded-md text-sm font-medium transition-colors ${
|
||||
pathname === item.href
|
||||
? 'text-primary font-semibold underline underline-offset-4'
|
||||
: 'text-neutral-700 dark:text-neutral-200 hover:text-primary'
|
||||
? 'text-blue-600 font-semibold underline underline-offset-4'
|
||||
: 'text-neutral-700 dark:text-neutral-200 hover:text-blue-600'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
|
||||
@@ -28,43 +28,43 @@ export default function ProductCard({ product, onAdd, added }: ProductCardProps)
|
||||
const restrictionConfig = {
|
||||
NFA: {
|
||||
label: 'NFA',
|
||||
color: 'badge-error',
|
||||
color: 'bg-red-100 text-red-800',
|
||||
icon: '🔒',
|
||||
tooltip: 'National Firearms Act - Requires special registration'
|
||||
},
|
||||
SBR: {
|
||||
label: 'SBR',
|
||||
color: 'badge-warning',
|
||||
color: 'bg-yellow-100 text-yellow-800',
|
||||
icon: '📏',
|
||||
tooltip: 'Short Barrel Rifle - Requires NFA registration'
|
||||
},
|
||||
SUPPRESSOR: {
|
||||
label: 'Suppressor',
|
||||
color: 'badge-secondary',
|
||||
color: 'bg-gray-100 text-gray-800',
|
||||
icon: '🔇',
|
||||
tooltip: 'Sound Suppressor - Requires NFA registration'
|
||||
},
|
||||
FFL_REQUIRED: {
|
||||
label: 'FFL',
|
||||
color: 'badge-info',
|
||||
color: 'bg-blue-100 text-blue-800',
|
||||
icon: '🏪',
|
||||
tooltip: 'Federal Firearms License required for purchase'
|
||||
},
|
||||
STATE_RESTRICTIONS: {
|
||||
label: 'State',
|
||||
color: 'badge-warning',
|
||||
color: 'bg-yellow-100 text-yellow-800',
|
||||
icon: '🗺️',
|
||||
tooltip: 'State-specific restrictions may apply'
|
||||
},
|
||||
HIGH_CAPACITY: {
|
||||
label: 'High Cap',
|
||||
color: 'badge-accent',
|
||||
color: 'bg-purple-100 text-purple-800',
|
||||
icon: '🥁',
|
||||
tooltip: 'High capacity magazine - check local laws'
|
||||
},
|
||||
SILENCERSHOP_PARTNER: {
|
||||
label: 'SilencerShop',
|
||||
color: 'badge-success',
|
||||
color: 'bg-green-100 text-green-800',
|
||||
icon: '🤝',
|
||||
tooltip: 'Available through SilencerShop partnership'
|
||||
}
|
||||
@@ -75,7 +75,7 @@ export default function ProductCard({ product, onAdd, added }: ProductCardProps)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`badge ${config.color} gap-1 cursor-help`}
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${config.color} gap-1 cursor-help`}
|
||||
title={config.tooltip}
|
||||
>
|
||||
<span>{config.icon}</span>
|
||||
@@ -85,7 +85,7 @@ export default function ProductCard({ product, onAdd, added }: ProductCardProps)
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card bg-base-100 shadow-lg hover:shadow-xl transition-shadow duration-300 border border-base-300">
|
||||
<div className="bg-white dark:bg-gray-800 shadow-lg hover:shadow-xl transition-shadow duration-300 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<figure className="relative">
|
||||
<img
|
||||
src={imageError ? '/window.svg' : product.image_url}
|
||||
@@ -102,25 +102,25 @@ export default function ProductCard({ product, onAdd, added }: ProductCardProps)
|
||||
)}
|
||||
</figure>
|
||||
|
||||
<div className="card-body">
|
||||
<h3 className="card-title text-base-content line-clamp-2">{product.name}</h3>
|
||||
<p className="text-base-content/70 text-sm line-clamp-2">{product.description}</p>
|
||||
<div className="p-4">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white line-clamp-2">{product.name}</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm line-clamp-2">{product.description}</p>
|
||||
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm text-base-content/60">{product.brand.name}</span>
|
||||
<span className="text-lg font-bold text-primary">${lowestPrice.toFixed(2)}</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">{product.brand.name}</span>
|
||||
<span className="text-lg font-bold text-blue-600 dark:text-blue-400">${lowestPrice.toFixed(2)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-base-content/50">{product.category.name}</span>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500">{product.category.name}</span>
|
||||
<Link href={`/products/${product.id}`} legacyBehavior>
|
||||
<a className="btn btn-primary btn-sm">
|
||||
View Details
|
||||
</a>
|
||||
<a className="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded-md text-sm font-medium transition-colors">
|
||||
View Details
|
||||
</a>
|
||||
</Link>
|
||||
{onAdd && (
|
||||
<button
|
||||
className="btn btn-neutral btn-sm ml-2 flex items-center gap-1"
|
||||
className="bg-gray-600 hover:bg-gray-700 text-white px-3 py-1 rounded-md text-sm font-medium transition-colors ml-2 flex items-center gap-1"
|
||||
onClick={onAdd}
|
||||
disabled={added}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user