Commit iniziale
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
const navItems = [
|
||||
{ href: '/admin', label: 'Dashboard', exact: true },
|
||||
{ href: '/admin/products', label: 'Products' },
|
||||
{ href: '/admin/product-types', label: 'Product Types' },
|
||||
{ href: '/admin/categories', label: 'Categories' },
|
||||
{ href: '/admin/orders', label: 'Orders' },
|
||||
{ href: '/admin/customers', label: 'Customers' },
|
||||
{ href: '/admin/reviews', label: 'Reviews' },
|
||||
{ href: '/admin/settings', label: 'Settings' },
|
||||
{ href: '/admin/admin-users', label: 'Admin Users' },
|
||||
]
|
||||
|
||||
export function AdminNav() {
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
|
||||
async function handleLogout() {
|
||||
await fetch('/api/auth/logout', { method: 'POST' })
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="bg-gray-900 text-white w-56 min-h-screen flex flex-col">
|
||||
<div className="px-4 py-5 border-b border-gray-700">
|
||||
<h1 className="text-lg font-bold">Admin Panel</h1>
|
||||
</div>
|
||||
<ul className="flex-1 py-4">
|
||||
{navItems.map((item) => {
|
||||
const active = item.exact ? pathname === item.href : pathname.startsWith(item.href)
|
||||
return (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={`block px-4 py-2 text-sm hover:bg-gray-700 transition-colors ${
|
||||
active ? 'bg-gray-700 text-white' : 'text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
<div className="px-4 py-4 border-t border-gray-700 space-y-2">
|
||||
<Link
|
||||
href="/admin/change-password"
|
||||
className="block text-sm text-gray-300 hover:text-white"
|
||||
>
|
||||
Change Password
|
||||
</Link>
|
||||
<Link href="/" className="block text-sm text-gray-300 hover:text-white">
|
||||
View Store
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="block text-sm text-gray-300 hover:text-white w-full text-left"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export function Navbar() {
|
||||
const [cartCount, setCartCount] = useState(0)
|
||||
const [user, setUser] = useState<{ name?: string; email: string; role: string } | null>(null)
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const cart = JSON.parse(localStorage.getItem('cart') || '[]')
|
||||
const count = cart.reduce((sum: number, item: { quantity: number }) => sum + item.quantity, 0)
|
||||
setCartCount(count)
|
||||
|
||||
const userData = localStorage.getItem('user')
|
||||
if (userData) {
|
||||
try {
|
||||
setUser(JSON.parse(userData))
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
async function handleLogout() {
|
||||
await fetch('/api/auth/logout', { method: 'POST' })
|
||||
localStorage.removeItem('user')
|
||||
setUser(null)
|
||||
router.push('/')
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="bg-white border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<Link href="/" className="text-xl font-bold text-gray-900">
|
||||
ShopX
|
||||
</Link>
|
||||
|
||||
<nav className="flex items-center gap-6 text-sm">
|
||||
<Link href="/products" className="text-gray-600 hover:text-gray-900">
|
||||
Products
|
||||
</Link>
|
||||
<Link href="/cart" className="text-gray-600 hover:text-gray-900 relative">
|
||||
Cart
|
||||
{cartCount > 0 && (
|
||||
<span className="ml-1 bg-blue-600 text-white text-xs px-1.5 py-0.5 rounded-full">
|
||||
{cartCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
{user ? (
|
||||
<>
|
||||
<Link href="/account" className="text-gray-600 hover:text-gray-900">
|
||||
Account
|
||||
</Link>
|
||||
{(user.role === 'ADMIN' || user.role === 'OWNER') && (
|
||||
<Link href="/admin" className="text-gray-600 hover:text-gray-900">
|
||||
Admin
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link href="/login" className="text-gray-600 hover:text-gray-900">
|
||||
Login
|
||||
</Link>
|
||||
<Link
|
||||
href="/register"
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Register
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
interface Product {
|
||||
id: string
|
||||
title: string
|
||||
slug: string
|
||||
basePrice: number
|
||||
currency: string
|
||||
images: Array<{ url: string; altText?: string | null }>
|
||||
}
|
||||
|
||||
interface ProductCardProps {
|
||||
product: Product
|
||||
}
|
||||
|
||||
export function ProductCard({ product }: ProductCardProps) {
|
||||
const price = (product.basePrice / 100).toFixed(2)
|
||||
const image = product.images[0]
|
||||
|
||||
return (
|
||||
<Link href={`/products/${product.slug}`} className="group">
|
||||
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden hover:shadow-md transition-shadow">
|
||||
<div className="aspect-square bg-gray-100 flex items-center justify-center">
|
||||
{image ? (
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.altText || product.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-gray-400 text-sm">No image</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<h3 className="font-medium text-gray-900 group-hover:text-blue-600 transition-colors line-clamp-2">
|
||||
{product.title}
|
||||
</h3>
|
||||
<p className="mt-1 text-lg font-bold text-gray-900">
|
||||
{price} {product.currency}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
interface AlertProps {
|
||||
children: React.ReactNode
|
||||
variant?: 'info' | 'success' | 'warning' | 'error'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Alert({ children, variant = 'info', className = '' }: AlertProps) {
|
||||
const variants = {
|
||||
info: 'bg-blue-50 border-blue-200 text-blue-800',
|
||||
success: 'bg-green-50 border-green-200 text-green-800',
|
||||
warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
|
||||
error: 'bg-red-50 border-red-200 text-red-800',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`rounded border px-4 py-3 text-sm ${variants[variant]} ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
|
||||
interface BadgeProps {
|
||||
children: React.ReactNode
|
||||
variant?: 'default' | 'success' | 'warning' | 'danger' | 'info'
|
||||
}
|
||||
|
||||
export function Badge({ children, variant = 'default' }: BadgeProps) {
|
||||
const variants = {
|
||||
default: 'bg-gray-100 text-gray-700',
|
||||
success: 'bg-green-100 text-green-700',
|
||||
warning: 'bg-yellow-100 text-yellow-700',
|
||||
danger: 'bg-red-100 text-red-700',
|
||||
info: 'bg-blue-100 text-blue-700',
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${variants[variant]}`}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary' | 'danger' | 'ghost'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export function Button({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
loading = false,
|
||||
disabled,
|
||||
children,
|
||||
className = '',
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
const base = 'inline-flex items-center justify-center font-medium rounded focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
|
||||
const variants = {
|
||||
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
|
||||
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-400',
|
||||
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
|
||||
ghost: 'bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-400',
|
||||
}
|
||||
|
||||
const sizes = {
|
||||
sm: 'px-3 py-1.5 text-sm',
|
||||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-6 py-3 text-base',
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`${base} ${variants[variant]} ${sizes[size]} ${className}`}
|
||||
disabled={disabled || loading}
|
||||
{...props}
|
||||
>
|
||||
{loading && (
|
||||
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
)}
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
interface CardProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Card({ children, className = '' }: CardProps) {
|
||||
return (
|
||||
<div className={`bg-white rounded-lg border border-gray-200 shadow-sm ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function CardHeader({ children, className = '' }: CardProps) {
|
||||
return (
|
||||
<div className={`px-6 py-4 border-b border-gray-200 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function CardBody({ children, className = '' }: CardProps) {
|
||||
return (
|
||||
<div className={`px-6 py-4 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from 'react'
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export function Input({ label, error, className = '', id, ...props }: InputProps) {
|
||||
const inputId = id || label?.toLowerCase().replace(/\s+/g, '-')
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{label && (
|
||||
<label htmlFor={inputId} className="block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
id={inputId}
|
||||
className={`block w-full rounded border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
error ? 'border-red-500' : 'border-gray-300'
|
||||
} ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
label?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export function Textarea({ label, error, className = '', id, ...props }: TextareaProps) {
|
||||
const inputId = id || label?.toLowerCase().replace(/\s+/g, '-')
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{label && (
|
||||
<label htmlFor={inputId} className="block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<textarea
|
||||
id={inputId}
|
||||
className={`block w-full rounded border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
error ? 'border-red-500' : 'border-gray-300'
|
||||
} ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||
label?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export function Select({ label, error, className = '', id, children, ...props }: SelectProps) {
|
||||
const inputId = id || label?.toLowerCase().replace(/\s+/g, '-')
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{label && (
|
||||
<label htmlFor={inputId} className="block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<select
|
||||
id={inputId}
|
||||
className={`block w-full rounded border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
error ? 'border-red-500' : 'border-gray-300'
|
||||
} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user