tailwinds custom theme and tw plus components

This commit is contained in:
2025-06-29 07:43:18 -04:00
parent cfcc4c480e
commit ec6a0861f0
12 changed files with 1114 additions and 280 deletions

View File

@@ -3,6 +3,7 @@
import { useState } from 'react';
import Link from 'next/link';
import React from 'react';
import SearchInput from '@/components/SearchInput';
// AR-15 Build Requirements grouped by main categories
const buildGroups = [
@@ -333,13 +334,11 @@ export default function BuildPage() {
{/* 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..."
<SearchInput
label="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"
onChange={setSearchTerm}
placeholder="Search components..."
/>
</div>
</div>
@@ -459,23 +458,23 @@ export default function BuildPage() {
return (
<React.Fragment key={group.name}>
{/* Group Header */}
<tr className="bg-gray-50">
<tr className="bg-gray-800">
<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">
<div className="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center">
<span className="text-white font-semibold text-lg">
{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>
<h3 className="text-xl font-bold text-white">{group.name}</h3>
<p className="text-gray-300">{group.description}</p>
</div>
<div className="ml-auto text-right">
<div className="text-sm text-gray-500">
<div className="text-gray-300 font-medium">
{groupComponents.length} components
</div>
</div>

View File

@@ -1,6 +1,7 @@
'use client';
import { useState } from 'react';
import SearchInput from '@/components/SearchInput';
// Sample build data
const sampleBuilds = [
@@ -252,13 +253,11 @@ export default function BuildsPage() {
{/* 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 Builds</label>
<input
type="text"
placeholder="Search builds..."
<SearchInput
label="Search Builds"
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"
onChange={setSearchTerm}
placeholder="Search builds..."
/>
</div>
</div>

View File

@@ -1,3 +1,56 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind utilities;
@layer base {
html {
scroll-behavior: smooth;
}
body {
@apply transition-colors duration-200;
}
}
@layer components {
/* Custom scrollbar for webkit browsers */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
@apply bg-neutral-100 dark:bg-neutral-800;
}
::-webkit-scrollbar-thumb {
@apply bg-neutral-300 dark:bg-neutral-600 rounded-full;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-neutral-400 dark:bg-neutral-500;
}
/* Focus styles for better accessibility */
.focus-ring {
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-neutral-900;
}
/* Card styles */
.card {
@apply bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200;
}
/* Button styles */
.btn-primary {
@apply bg-primary-600 hover:bg-primary-700 dark:bg-primary-500 dark:hover:bg-primary-600 text-white font-medium py-2 px-4 rounded-lg transition-colors duration-200 focus-ring;
}
.btn-secondary {
@apply bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-700 dark:hover:bg-neutral-600 text-neutral-700 dark:text-neutral-300 font-medium py-2 px-4 rounded-lg transition-colors duration-200 focus-ring;
}
/* Input styles */
.input-field {
@apply w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white placeholder-neutral-500 dark:placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-colors duration-200;
}
}

View File

@@ -1,21 +1,14 @@
import "./globals.css";
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Navbar } from "@/components/Navbar";
import { Inter } from "next/font/google";
import Navbar from "@/components/Navbar";
import { ThemeProvider } from "@/components/ThemeProvider";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Pew Builder - Firearm Parts Catalog",
description: "Build your dream AR-15 with our comprehensive parts catalog and build checklist",
title: "Pew Builder - Firearm Parts Catalog & Build Management",
description: "Professional firearm parts catalog and AR-15 build management system",
};
export default function RootLayout({
@@ -24,10 +17,14 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable}`}>
<Navbar />
{children}
<html lang="en" suppressHydrationWarning>
<body className={`${inter.className} antialiased`}>
<ThemeProvider>
<div className="min-h-screen bg-neutral-50 dark:bg-neutral-900 transition-colors duration-200">
<Navbar />
{children}
</div>
</ThemeProvider>
</body>
</html>
);

File diff suppressed because it is too large Load Diff

View File

@@ -2,52 +2,56 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import ThemeSwitcher from './ThemeSwitcher';
export function Navbar() {
export default function Navbar() {
const pathname = usePathname();
const navItems = [
{ name: 'Parts Catalog', href: '/' },
{ name: 'Build Checklist', href: '/build' },
{ name: 'My Builds', href: '/builds' },
{ href: '/', label: 'Parts Catalog' },
{ href: '/build', label: 'Build Checklist' },
{ href: '/builds', label: 'My Builds' },
];
return (
<nav className="bg-white shadow-sm border-b">
<nav className="bg-white dark:bg-neutral-800 border-b border-neutral-200 dark:border-neutral-700 shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo/Brand */}
{/* Logo */}
<div className="flex items-center">
<Link href="/" className="text-2xl font-bold text-gray-900">
Pew Builder
<Link href="/" className="flex items-center space-x-2">
<div className="w-8 h-8 bg-primary-600 dark:bg-primary-500 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-lg">🔫</span>
</div>
<span className="text-xl font-bold text-neutral-900 dark:text-white">
Pew Builder
</span>
</Link>
</div>
{/* Navigation Links */}
<div className="hidden md:block">
<div className="ml-10 flex items-baseline space-x-4">
{navItems.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
isActive
? 'bg-blue-600 text-white'
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-900'
}`}
>
{item.name}
</Link>
);
})}
</div>
<div className="hidden md:flex items-center space-x-8">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
pathname === item.href
? 'bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300'
: 'text-neutral-600 dark:text-neutral-300 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-700'
}`}
>
{item.label}
</Link>
))}
</div>
{/* Mobile menu button */}
<div className="md:hidden">
<button className="text-gray-700 hover:text-gray-900 focus:outline-none focus:text-gray-900">
{/* Theme Switcher */}
<div className="flex items-center space-x-4">
<ThemeSwitcher />
{/* Mobile menu button */}
<button className="md:hidden p-2 rounded-md text-neutral-600 dark:text-neutral-300 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-700">
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
@@ -63,7 +67,7 @@ export function Navbar() {
const isActive = pathname === item.href;
return (
<Link
key={item.name}
key={item.href}
href={item.href}
className={`block px-3 py-2 rounded-md text-base font-medium transition-colors ${
isActive
@@ -71,7 +75,7 @@ export function Navbar() {
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-900'
}`}
>
{item.name}
{item.label}
</Link>
);
})}

View File

@@ -0,0 +1,39 @@
import { MagnifyingGlassIcon } from '@heroicons/react/20/solid';
interface SearchInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
label?: string;
className?: string;
}
export default function SearchInput({
value,
onChange,
placeholder = "Search...",
label,
className = ""
}: SearchInputProps) {
return (
<div className={className}>
{label && (
<label className="block text-sm font-medium text-gray-700 mb-1">
{label}
</label>
)}
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="text"
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,52 @@
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('light');
useEffect(() => {
// Check for saved theme preference or default to light mode
const savedTheme = localStorage.getItem('theme') as Theme;
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme) {
setTheme(savedTheme);
} else if (prefersDark) {
setTheme('dark');
}
}, []);
useEffect(() => {
// Update document class and save preference
document.documentElement.classList.toggle('dark', theme === 'dark');
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

View File

@@ -0,0 +1,28 @@
'use client';
import { SunIcon, MoonIcon } from '@heroicons/react/24/outline';
import { useTheme } from './ThemeProvider';
export default function ThemeSwitcher() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 dark:bg-gray-700 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
>
<span
className={`inline-block h-6 w-6 transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out ${
theme === 'dark' ? 'translate-x-7' : 'translate-x-1'
}`}
>
{theme === 'light' ? (
<SunIcon className="h-6 w-6 text-yellow-500" />
) : (
<MoonIcon className="h-6 w-6 text-blue-400" />
)}
</span>
</button>
);
}