Enhance UI components and animations: Introduce a shimmer animation effect in globals.css, refactor FeedbackPageClient, LoginPage, RegisterPage, and AdminPanel components to utilize new UI components for improved consistency and maintainability. Update event and feedback handling in EventsPageSection and FeedbackModal, ensuring a cohesive user experience across the application.
This commit is contained in:
32
components/ui/Alert.tsx
Normal file
32
components/ui/Alert.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { HTMLAttributes, ReactNode } from "react";
|
||||
|
||||
interface AlertProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children: ReactNode;
|
||||
variant?: "success" | "error" | "warning" | "info";
|
||||
}
|
||||
|
||||
const variantClasses = {
|
||||
success: "bg-green-900/50 border-green-500/50 text-green-400",
|
||||
error: "bg-red-900/50 border-red-500/50 text-red-400",
|
||||
warning: "bg-yellow-900/50 border-yellow-500/50 text-yellow-400",
|
||||
info: "bg-blue-900/50 border-blue-500/50 text-blue-400",
|
||||
};
|
||||
|
||||
export default function Alert({
|
||||
children,
|
||||
variant = "info",
|
||||
className = "",
|
||||
...props
|
||||
}: AlertProps) {
|
||||
return (
|
||||
<div
|
||||
className={`border rounded px-4 py-3 text-sm ${variantClasses[variant]} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
70
components/ui/Avatar.tsx
Normal file
70
components/ui/Avatar.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { normalizeAvatarUrl } from "@/lib/avatars";
|
||||
|
||||
interface AvatarProps {
|
||||
src: string | null | undefined;
|
||||
username: string;
|
||||
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
||||
className?: string;
|
||||
borderClassName?: string;
|
||||
fallbackText?: string;
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
xs: "w-6 h-6 text-[8px]",
|
||||
sm: "w-8 h-8 text-[10px]",
|
||||
md: "w-10 h-10 text-xs",
|
||||
lg: "w-16 h-16 sm:w-20 sm:h-20 text-xl sm:text-2xl",
|
||||
xl: "w-24 h-24 text-4xl",
|
||||
"2xl": "w-32 h-32 text-4xl",
|
||||
};
|
||||
|
||||
export default function Avatar({
|
||||
src,
|
||||
username,
|
||||
size = "md",
|
||||
className = "",
|
||||
borderClassName = "",
|
||||
fallbackText,
|
||||
}: AvatarProps) {
|
||||
const [avatarError, setAvatarError] = useState(false);
|
||||
const prevSrcRef = useRef<string | null | undefined>(undefined);
|
||||
|
||||
// Reset error state when src changes
|
||||
useEffect(() => {
|
||||
if (src !== prevSrcRef.current) {
|
||||
prevSrcRef.current = src;
|
||||
// Reset error when src changes to allow retry
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setAvatarError(false);
|
||||
}
|
||||
}, [src]);
|
||||
|
||||
const sizeClass = sizeClasses[size];
|
||||
const normalizedSrc = normalizeAvatarUrl(src);
|
||||
const displaySrc = normalizedSrc && !avatarError ? normalizedSrc : null;
|
||||
const initial = fallbackText || username.charAt(0).toUpperCase();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${sizeClass} rounded-full border overflow-hidden bg-black/60 flex items-center justify-center relative ${className} ${borderClassName}`}
|
||||
>
|
||||
{displaySrc ? (
|
||||
<img
|
||||
key={displaySrc}
|
||||
src={displaySrc}
|
||||
alt={username}
|
||||
className="w-full h-full object-cover absolute inset-0"
|
||||
onError={() => setAvatarError(true)}
|
||||
/>
|
||||
) : null}
|
||||
<span
|
||||
className={`text-pixel-gold font-bold ${displaySrc ? "hidden" : ""}`}
|
||||
>
|
||||
{initial}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
components/ui/BackgroundSection.tsx
Normal file
43
components/ui/BackgroundSection.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { HTMLAttributes, ReactNode } from "react";
|
||||
|
||||
interface BackgroundSectionProps extends HTMLAttributes<HTMLElement> {
|
||||
children: ReactNode;
|
||||
backgroundImage: string;
|
||||
overlay?: boolean;
|
||||
}
|
||||
|
||||
export default function BackgroundSection({
|
||||
children,
|
||||
backgroundImage,
|
||||
overlay = true,
|
||||
className = "",
|
||||
...props
|
||||
}: BackgroundSectionProps) {
|
||||
return (
|
||||
<section
|
||||
className={`relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16 ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{/* Background Image */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('${backgroundImage}')`,
|
||||
}}
|
||||
>
|
||||
{/* Dark overlay for readability */}
|
||||
{overlay && (
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 w-full max-w-6xl mx-auto px-4 sm:px-8 py-16">
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
40
components/ui/Badge.tsx
Normal file
40
components/ui/Badge.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { HTMLAttributes, ReactNode } from "react";
|
||||
|
||||
interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
children: ReactNode;
|
||||
variant?: "default" | "success" | "warning" | "danger" | "info";
|
||||
size?: "sm" | "md";
|
||||
}
|
||||
|
||||
const variantClasses = {
|
||||
default: "bg-pixel-gold/20 border-pixel-gold/50 text-pixel-gold",
|
||||
success: "bg-green-900/50 border-green-500/50 text-green-400",
|
||||
warning: "bg-yellow-900/50 border-yellow-500/50 text-yellow-400",
|
||||
danger: "bg-red-900/50 border-red-500/50 text-red-400",
|
||||
info: "bg-blue-900/30 border-blue-500/50 text-blue-400",
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "px-2 py-1 text-[10px] sm:text-xs",
|
||||
md: "px-3 py-1 text-xs",
|
||||
};
|
||||
|
||||
export default function Badge({
|
||||
children,
|
||||
variant = "default",
|
||||
size = "sm",
|
||||
className = "",
|
||||
...props
|
||||
}: BadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={`inline-block border uppercase rounded whitespace-nowrap flex-shrink-0 ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
47
components/ui/Button.tsx
Normal file
47
components/ui/Button.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { ButtonHTMLAttributes, ReactNode, ElementType } from "react";
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: "primary" | "secondary" | "success" | "danger" | "ghost";
|
||||
size?: "sm" | "md" | "lg";
|
||||
children: ReactNode;
|
||||
as?: ElementType;
|
||||
}
|
||||
|
||||
const variantClasses = {
|
||||
primary:
|
||||
"border-pixel-gold/50 bg-black/60 text-white hover:bg-pixel-gold/10 hover:border-pixel-gold",
|
||||
secondary:
|
||||
"border-gray-600/50 bg-gray-900/20 text-gray-400 hover:bg-gray-900/30 hover:border-gray-500",
|
||||
success:
|
||||
"border-green-500/50 bg-green-900/20 text-green-400 hover:bg-green-900/30",
|
||||
danger: "border-red-500/50 bg-red-900/20 text-red-400 hover:bg-red-900/30",
|
||||
ghost: "border-transparent bg-transparent text-white hover:text-pixel-gold",
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "px-2 sm:px-3 py-1 text-[10px] sm:text-xs",
|
||||
md: "px-4 py-2 text-xs",
|
||||
lg: "px-6 py-3 text-sm",
|
||||
};
|
||||
|
||||
export default function Button({
|
||||
variant = "primary",
|
||||
size = "md",
|
||||
className = "",
|
||||
disabled,
|
||||
children,
|
||||
as: Component = "button",
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<Component
|
||||
className={`border uppercase tracking-widest rounded transition disabled:opacity-50 disabled:cursor-not-allowed ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
30
components/ui/Card.tsx
Normal file
30
components/ui/Card.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { HTMLAttributes, ReactNode } from "react";
|
||||
|
||||
interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children: ReactNode;
|
||||
variant?: "default" | "dark";
|
||||
}
|
||||
|
||||
const variantClasses = {
|
||||
default: "bg-black/60 border border-pixel-gold/30",
|
||||
dark: "bg-black/80 border border-pixel-gold/30",
|
||||
};
|
||||
|
||||
export default function Card({
|
||||
children,
|
||||
variant = "default",
|
||||
className = "",
|
||||
...props
|
||||
}: CardProps) {
|
||||
return (
|
||||
<div
|
||||
className={`rounded-lg backdrop-blur-sm ${variantClasses[variant]} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
29
components/ui/CloseButton.tsx
Normal file
29
components/ui/CloseButton.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { ButtonHTMLAttributes } from "react";
|
||||
|
||||
interface CloseButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "text-xl",
|
||||
md: "text-2xl",
|
||||
lg: "text-3xl",
|
||||
};
|
||||
|
||||
export default function CloseButton({
|
||||
size = "md",
|
||||
className = "",
|
||||
...props
|
||||
}: CloseButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={`text-gray-400 hover:text-pixel-gold font-bold transition disabled:opacity-50 disabled:cursor-not-allowed ${sizeClasses[size]} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
38
components/ui/Input.tsx
Normal file
38
components/ui/Input.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { InputHTMLAttributes, forwardRef } from "react";
|
||||
|
||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ label, error, className = "", ...props }, ref) => {
|
||||
return (
|
||||
<div>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={props.id}
|
||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
className={`w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
{error && (
|
||||
<p className="text-red-400 text-xs mt-1">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Input.displayName = "Input";
|
||||
|
||||
export default Input;
|
||||
|
||||
54
components/ui/Modal.tsx
Normal file
54
components/ui/Modal.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, useEffect } from "react";
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
size?: "sm" | "md" | "lg" | "xl";
|
||||
closeOnOverlayClick?: boolean;
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "max-w-md",
|
||||
md: "max-w-2xl",
|
||||
lg: "max-w-3xl",
|
||||
xl: "max-w-4xl",
|
||||
};
|
||||
|
||||
export default function Modal({
|
||||
isOpen,
|
||||
onClose,
|
||||
children,
|
||||
size = "md",
|
||||
closeOnOverlayClick = true,
|
||||
}: ModalProps) {
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
||||
onClick={closeOnOverlayClick ? onClose : undefined}
|
||||
>
|
||||
<div
|
||||
className={`bg-black border-2 border-pixel-gold/70 rounded-lg w-full ${sizeClasses[size]} max-h-[90vh] overflow-y-auto shadow-2xl`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
73
components/ui/ProgressBar.tsx
Normal file
73
components/ui/ProgressBar.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { HTMLAttributes } from "react";
|
||||
|
||||
interface ProgressBarProps extends HTMLAttributes<HTMLDivElement> {
|
||||
value: number;
|
||||
max: number;
|
||||
variant?: "hp" | "xp" | "default";
|
||||
showLabel?: boolean;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const variantClasses = {
|
||||
hp: {
|
||||
high: "from-green-600 to-green-700",
|
||||
medium: "from-yellow-600 to-orange-700",
|
||||
low: "from-red-700 to-red-900",
|
||||
},
|
||||
xp: "from-pixel-gold/80 via-pixel-gold/70 to-pixel-gold/80",
|
||||
default: "from-pixel-gold/80 via-pixel-gold/70 to-pixel-gold/80",
|
||||
};
|
||||
|
||||
export default function ProgressBar({
|
||||
value,
|
||||
max,
|
||||
variant = "default",
|
||||
showLabel = false,
|
||||
label,
|
||||
className = "",
|
||||
...props
|
||||
}: ProgressBarProps) {
|
||||
const percentage = Math.min(100, Math.max(0, (value / max) * 100));
|
||||
|
||||
let gradientClass = "";
|
||||
if (variant === "hp") {
|
||||
if (percentage > 60) {
|
||||
gradientClass = variantClasses.hp.high;
|
||||
} else if (percentage > 30) {
|
||||
gradientClass = variantClasses.hp.medium;
|
||||
} else {
|
||||
gradientClass = variantClasses.hp.low;
|
||||
}
|
||||
} else if (variant === "xp") {
|
||||
gradientClass = variantClasses.xp;
|
||||
} else {
|
||||
gradientClass = variantClasses.default;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} {...props}>
|
||||
{showLabel && (
|
||||
<div className="flex justify-between text-xs text-gray-400 mb-1">
|
||||
<span>{label || variant.toUpperCase()}</span>
|
||||
<span>
|
||||
{value} / {max}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative h-2 sm:h-3 bg-gray-900 border border-gray-700 rounded overflow-hidden">
|
||||
<div
|
||||
className={`absolute inset-0 bg-gradient-to-r ${gradientClass} transition-all duration-1000 ease-out`}
|
||||
style={{ width: `${percentage}%` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-shimmer"></div>
|
||||
</div>
|
||||
{variant === "hp" && percentage < 30 && (
|
||||
<div className="absolute inset-0 border border-red-500 rounded animate-pulse"></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
64
components/ui/SectionTitle.tsx
Normal file
64
components/ui/SectionTitle.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { HTMLAttributes, ReactNode } from "react";
|
||||
|
||||
interface SectionTitleProps extends HTMLAttributes<HTMLHeadingElement> {
|
||||
children: ReactNode;
|
||||
variant?: "default" | "gradient" | "gold";
|
||||
size?: "sm" | "md" | "lg" | "xl";
|
||||
subtitle?: ReactNode;
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "text-2xl sm:text-3xl",
|
||||
md: "text-3xl sm:text-4xl md:text-5xl",
|
||||
lg: "text-4xl sm:text-5xl md:text-6xl lg:text-7xl",
|
||||
xl: "text-5xl md:text-7xl",
|
||||
};
|
||||
|
||||
export default function SectionTitle({
|
||||
children,
|
||||
variant = "default",
|
||||
size = "md",
|
||||
subtitle,
|
||||
className = "",
|
||||
...props
|
||||
}: SectionTitleProps) {
|
||||
const baseClasses = "font-gaming font-black tracking-tight mb-4";
|
||||
|
||||
let titleClasses = `${baseClasses} ${sizeClasses[size]} ${className}`;
|
||||
|
||||
if (variant === "gradient") {
|
||||
titleClasses += " bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent";
|
||||
} else if (variant === "gold") {
|
||||
titleClasses += " text-pixel-gold";
|
||||
} else {
|
||||
titleClasses += " text-white";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<h1 className={titleClasses} {...props}>
|
||||
{variant === "gradient" ? (
|
||||
<span
|
||||
style={{
|
||||
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</h1>
|
||||
{subtitle && (
|
||||
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 tracking-wide">
|
||||
<span>✦</span>
|
||||
<span>{subtitle}</span>
|
||||
<span>✦</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
66
components/ui/StarRating.tsx
Normal file
66
components/ui/StarRating.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface StarRatingProps {
|
||||
value: number;
|
||||
onChange?: (rating: number) => void;
|
||||
disabled?: boolean;
|
||||
size?: "sm" | "md" | "lg";
|
||||
showValue?: boolean;
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "text-lg sm:text-xl",
|
||||
md: "text-2xl sm:text-3xl",
|
||||
lg: "text-4xl",
|
||||
};
|
||||
|
||||
export default function StarRating({
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
size = "md",
|
||||
showValue = false,
|
||||
}: StarRatingProps) {
|
||||
const [hoverValue, setHoverValue] = useState(0);
|
||||
|
||||
const handleClick = (rating: number) => {
|
||||
if (!disabled && onChange) {
|
||||
onChange(rating);
|
||||
}
|
||||
};
|
||||
|
||||
const displayValue = hoverValue || value;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<button
|
||||
key={star}
|
||||
type="button"
|
||||
onClick={() => handleClick(star)}
|
||||
disabled={disabled}
|
||||
onMouseEnter={() => !disabled && setHoverValue(star)}
|
||||
onMouseLeave={() => !disabled && setHoverValue(0)}
|
||||
className={`transition-transform hover:scale-110 disabled:hover:scale-100 disabled:cursor-not-allowed ${
|
||||
star <= displayValue
|
||||
? "text-pixel-gold"
|
||||
: "text-gray-600 hover:text-gray-500"
|
||||
} ${sizeClasses[size]}`}
|
||||
aria-label={`Noter ${star} étoile${star > 1 ? "s" : ""}`}
|
||||
>
|
||||
★
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{showValue && value > 0 && (
|
||||
<p className="text-gray-500 text-xs text-center">
|
||||
{value}/5
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
49
components/ui/Textarea.tsx
Normal file
49
components/ui/Textarea.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { TextareaHTMLAttributes, forwardRef } from "react";
|
||||
|
||||
interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
showCharCount?: boolean;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ label, error, showCharCount, maxLength, className = "", value, ...props }, ref) => {
|
||||
const charCount = typeof value === "string" ? value.length : 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={props.id}
|
||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<textarea
|
||||
ref={ref}
|
||||
maxLength={maxLength}
|
||||
value={value}
|
||||
className={`w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition resize-none ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
{showCharCount && maxLength && (
|
||||
<p className="text-gray-500 text-xs mt-1 text-right">
|
||||
{charCount}/{maxLength} caractères
|
||||
</p>
|
||||
)}
|
||||
{error && (
|
||||
<p className="text-red-400 text-xs mt-1">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Textarea.displayName = "Textarea";
|
||||
|
||||
export default Textarea;
|
||||
|
||||
14
components/ui/index.ts
Normal file
14
components/ui/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export { default as Avatar } from "./Avatar";
|
||||
export { default as Button } from "./Button";
|
||||
export { default as Input } from "./Input";
|
||||
export { default as Textarea } from "./Textarea";
|
||||
export { default as Card } from "./Card";
|
||||
export { default as Modal } from "./Modal";
|
||||
export { default as Badge } from "./Badge";
|
||||
export { default as ProgressBar } from "./ProgressBar";
|
||||
export { default as StarRating } from "./StarRating";
|
||||
export { default as SectionTitle } from "./SectionTitle";
|
||||
export { default as BackgroundSection } from "./BackgroundSection";
|
||||
export { default as Alert } from "./Alert";
|
||||
export { default as CloseButton } from "./CloseButton";
|
||||
|
||||
Reference in New Issue
Block a user