diff --git a/apps/react/src/app/components/datepicker/datepicker.scss b/apps/react/src/app/components/datepicker/datepicker.scss new file mode 100644 index 000000000..a3c5e96b0 --- /dev/null +++ b/apps/react/src/app/components/datepicker/datepicker.scss @@ -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; + } + } +} diff --git a/apps/react/src/app/components/datepicker/datepicker.tsx b/apps/react/src/app/components/datepicker/datepicker.tsx new file mode 100644 index 000000000..330a695cb --- /dev/null +++ b/apps/react/src/app/components/datepicker/datepicker.tsx @@ -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 = ({ + 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 ( +
+ setIsOpen(!isOpen)} + readOnly + className="datepicker__input" + data-testid="datepicker-input" + /> + {isOpen && ( +
+
+ +
{currentDate.toLocaleString('default', { month: 'long', year: 'numeric' })}
+ +
+
+ {emptyDays.map(i =>
)} + {days.map(day => ( + + ))} +
+
+ )} +
+ ) +} diff --git a/apps/react/src/app/components/datepicker/index.ts b/apps/react/src/app/components/datepicker/index.ts new file mode 100644 index 000000000..ae3801ad9 --- /dev/null +++ b/apps/react/src/app/components/datepicker/index.ts @@ -0,0 +1,2 @@ +export { DatePicker } from './datepicker' +export type { DatePickerProps } from './datepicker' diff --git a/apps/react/src/app/components/modal/index.ts b/apps/react/src/app/components/modal/index.ts new file mode 100644 index 000000000..1bdc2be39 --- /dev/null +++ b/apps/react/src/app/components/modal/index.ts @@ -0,0 +1,2 @@ +export { Modal } from './modal' +export type { ModalProps } from './modal' diff --git a/apps/react/src/app/components/modal/modal.scss b/apps/react/src/app/components/modal/modal.scss new file mode 100644 index 000000000..c2f12494d --- /dev/null +++ b/apps/react/src/app/components/modal/modal.scss @@ -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; + } +} diff --git a/apps/react/src/app/components/modal/modal.tsx b/apps/react/src/app/components/modal/modal.tsx new file mode 100644 index 000000000..858eed3c0 --- /dev/null +++ b/apps/react/src/app/components/modal/modal.tsx @@ -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 = ({ + isOpen, + onClose, + title, + children, + size = 'medium', + footer, +}) => { + if (!isOpen) return null + + return ( +
+
+ {title &&
+

{title}

+ +
} +
{children}
+ {footer &&
{footer}
} +
+
+ ) +} diff --git a/apps/react/src/app/components/tabs/index.ts b/apps/react/src/app/components/tabs/index.ts new file mode 100644 index 000000000..57fab1974 --- /dev/null +++ b/apps/react/src/app/components/tabs/index.ts @@ -0,0 +1,2 @@ +export { Tabs } from './tabs' +export type { TabsProps, TabItem } from './tabs' diff --git a/apps/react/src/app/components/tabs/tabs.scss b/apps/react/src/app/components/tabs/tabs.scss new file mode 100644 index 000000000..e813aa816 --- /dev/null +++ b/apps/react/src/app/components/tabs/tabs.scss @@ -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; + } +} diff --git a/apps/react/src/app/components/tabs/tabs.tsx b/apps/react/src/app/components/tabs/tabs.tsx new file mode 100644 index 000000000..acd43d5ff --- /dev/null +++ b/apps/react/src/app/components/tabs/tabs.tsx @@ -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 = ({ + 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 ( +
+
+ {tabs.map(tab => ( + + ))} +
+ {activeTab &&
{activeTab.content}
} +
+ ) +}