Task 8: Create Modal, Tabs, and DatePicker components

- Modal component with size variants (small, medium, large)
- Tabs component with active state management and onChange callback
- DatePicker component with calendar interface and date selection
- All components implement BEM naming convention
- All components include data-testid attributes for testing
- SCSS files provide complete styling for each component
- Index files export components and types for easy importing
This commit is contained in:
gnezim
2026-04-05 19:20:06 +03:00
parent 9356945d93
commit 4b34a78890
9 changed files with 331 additions and 0 deletions
@@ -0,0 +1,61 @@
.datepicker {
position: relative;
display: inline-block;
width: 100%;
&__input {
width: 100%;
padding: 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
&__calendar {
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 12px;
margin-top: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 10;
min-width: 300px;
}
&__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
button {
background: none;
border: none;
cursor: pointer;
font-size: 16px;
}
}
&__grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
}
&__day {
padding: 8px 4px;
background: none;
border: none;
cursor: pointer;
font-size: 12px;
border-radius: 4px;
&:hover {
background-color: #f0f0f0;
}
}
}
@@ -0,0 +1,73 @@
import React, { useState } from 'react'
import './datepicker.scss'
export interface DatePickerProps {
value?: Date
onChange: (date: Date) => void
placeholder?: string
minDate?: Date
maxDate?: Date
}
export const DatePicker: React.FC<DatePickerProps> = ({
value,
onChange,
placeholder = 'Select date',
minDate: _minDate,
maxDate: _maxDate,
}) => {
const [isOpen, setIsOpen] = useState(false)
const [currentDate, setCurrentDate] = useState(value || new Date())
const handleDateClick = (day: number) => {
const selected = new Date(currentDate.getFullYear(), currentDate.getMonth(), day)
onChange(selected)
setIsOpen(false)
}
const daysInMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate()
const firstDay = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1).getDay()
const days = Array.from({ length: daysInMonth }, (_, i) => i + 1)
const emptyDays = Array.from({ length: firstDay }, (_, i) => i)
return (
<div className="datepicker" data-testid="datepicker">
<input
type="text"
value={value ? value.toISOString().split('T')[0] : ''}
placeholder={placeholder}
onClick={() => setIsOpen(!isOpen)}
readOnly
className="datepicker__input"
data-testid="datepicker-input"
/>
{isOpen && (
<div className="datepicker__calendar">
<div className="datepicker__header">
<button onClick={() => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1))}>
</button>
<div>{currentDate.toLocaleString('default', { month: 'long', year: 'numeric' })}</div>
<button onClick={() => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1))}>
</button>
</div>
<div className="datepicker__grid">
{emptyDays.map(i => <div key={`empty-${i}`} />)}
{days.map(day => (
<button
key={day}
onClick={() => handleDateClick(day)}
className="datepicker__day"
data-testid={`day-${day}`}
>
{day}
</button>
))}
</div>
</div>
)}
</div>
)
}
@@ -0,0 +1,2 @@
export { DatePicker } from './datepicker'
export type { DatePickerProps } from './datepicker'
@@ -0,0 +1,2 @@
export { Modal } from './modal'
export type { ModalProps } from './modal'
@@ -0,0 +1,74 @@
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
max-height: 90vh;
overflow-y: auto;
&--small {
width: 100%;
max-width: 400px;
}
&--medium {
width: 100%;
max-width: 600px;
}
&--large {
width: 100%;
max-width: 800px;
}
&__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #e0e0e0;
}
&__title {
margin: 0;
font-size: 18px;
font-weight: 600;
}
&__close {
background: none;
border: none;
font-size: 28px;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
&__body {
padding: 20px;
}
&__footer {
padding: 20px;
border-top: 1px solid #e0e0e0;
display: flex;
justify-content: flex-end;
gap: 10px;
}
}
@@ -0,0 +1,35 @@
import React from 'react'
import './modal.scss'
export interface ModalProps {
isOpen: boolean
onClose: () => void
title?: string
children: React.ReactNode
size?: 'small' | 'medium' | 'large'
footer?: React.ReactNode
}
export const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
children,
size = 'medium',
footer,
}) => {
if (!isOpen) return null
return (
<div className="modal-overlay" data-testid="modal-overlay">
<div className={`modal modal--${size}`} data-testid="modal">
{title && <div className="modal__header">
<h2 className="modal__title">{title}</h2>
<button className="modal__close" onClick={onClose} data-testid="modal-close">×</button>
</div>}
<div className="modal__body">{children}</div>
{footer && <div className="modal__footer">{footer}</div>}
</div>
</div>
)
}
@@ -0,0 +1,2 @@
export { Tabs } from './tabs'
export type { TabsProps, TabItem } from './tabs'
@@ -0,0 +1,35 @@
.tabs {
width: 100%;
&__header {
display: flex;
border-bottom: 2px solid #e0e0e0;
gap: 0;
}
&__tab {
background: none;
border: none;
padding: 12px 24px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: #666;
position: relative;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
&:hover {
color: #333;
}
&--active {
color: #000;
border-bottom-color: #000;
}
}
&__content {
padding: 20px 0;
}
}
@@ -0,0 +1,47 @@
import React, { useState } from 'react'
import './tabs.scss'
export interface TabItem {
id: string
label: string
content: React.ReactNode
}
export interface TabsProps {
tabs: TabItem[]
defaultTabId?: string
onChange?: (tabId: string) => void
}
export const Tabs: React.FC<TabsProps> = ({
tabs,
defaultTabId,
onChange,
}) => {
const [activeId, setActiveId] = useState(defaultTabId || tabs[0]?.id)
const handleTabClick = (id: string) => {
setActiveId(id)
onChange?.(id)
}
const activeTab = tabs.find(t => t.id === activeId)
return (
<div className="tabs" data-testid="tabs">
<div className="tabs__header">
{tabs.map(tab => (
<button
key={tab.id}
className={`tabs__tab ${activeId === tab.id ? 'tabs__tab--active' : ''}`}
onClick={() => handleTabClick(tab.id)}
data-testid={`tab-${tab.id}`}
>
{tab.label}
</button>
))}
</div>
{activeTab && <div className="tabs__content">{activeTab.content}</div>}
</div>
)
}