Commit iniziale

This commit is contained in:
2026-05-18 15:25:38 +02:00
commit a8d4c158b8
79 changed files with 8730 additions and 0 deletions
+69
View File
@@ -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>
)
}
+90
View File
@@ -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>
)
}
+20
View File
@@ -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>
)
}
+22
View File
@@ -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>
)
}
+50
View File
@@ -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>
)
}
+28
View File
@@ -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>
)
}
+84
View File
@@ -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>
)
}