diff --git a/apps/web/.storybook/preview.tsx b/apps/web/.storybook/preview.tsx index 4e346cb..3f2996f 100644 --- a/apps/web/.storybook/preview.tsx +++ b/apps/web/.storybook/preview.tsx @@ -1,6 +1,7 @@ import React from "react"; import type { Preview } from "@storybook/react"; import "../src/App.css"; +import "../src/index.css"; const preview: Preview = { parameters: { diff --git a/apps/web/package.json b/apps/web/package.json index 4fc48f0..1a1d955 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -22,20 +22,21 @@ "@eslint/js": "^9.13.0", "@rentment/eslint-config": "workspace:*", "@rentment/typescript-config": "workspace:*", - "@storybook/addon-essentials": "^8.4.2", - "@storybook/addon-interactions": "^8.4.2", - "@storybook/addon-onboarding": "^8.4.2", - "@storybook/blocks": "^8.4.2", - "@storybook/react": "^8.4.2", - "@storybook/react-vite": "^8.4.2", - "@storybook/test": "^8.4.2", + "@storybook/addon-essentials": "^8.4.7", + "@storybook/addon-interactions": "^8.4.7", + "@storybook/addon-onboarding": "^8.4.7", + "@storybook/blocks": "^8.4.7", + "@storybook/react": "^8.4.7", + "@storybook/react-vite": "^8.4.7", + "@storybook/test": "^8.4.7", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.3", "eslint-plugin-storybook": "^0.11.0", "globals": "^15.11.0", - "storybook": "^8.4.2", + "jsdoc-type-pratt-parser": "^4.1.0", + "storybook": "^8.4.7", "typescript": "~5.6.2", - "vite": "^5.4.10" + "vite": "^5.4.11" } } diff --git a/apps/web/src/App.css b/apps/web/src/App.css index b9d355d..82e2935 100644 --- a/apps/web/src/App.css +++ b/apps/web/src/App.css @@ -1,42 +1,5 @@ #root { - max-width: 1280px; + /* max-width: 1280px; */ margin: 0 auto; - padding: 2rem; text-align: center; } - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 7d4aa37..bb4294d 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,143 +1,29 @@ -import { useState } from "react"; -import reactLogo from "./assets/react.svg"; -import viteLogo from "/vite.svg"; +import { BaseWrapper } from "./components/BaseComponents/BaseWrapper"; import "./App.css"; -import { FaSignInAlt } from "react-icons/fa"; -import { Button } from "./components"; +// import reactLogo from './assets/react.svg'; +// import viteLogo from '/vite.svg'; +// import { FaSignInAlt } from 'react-icons/fa'; +// import { Button } from './components'; function App() { - const [count, setCount] = useState(0); - return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

-
-
- + + Hello! + ); } diff --git a/apps/web/src/components/BaseComponents/BaseWrapper.css b/apps/web/src/components/BaseComponents/BaseWrapper.css new file mode 100644 index 0000000..55bc480 --- /dev/null +++ b/apps/web/src/components/BaseComponents/BaseWrapper.css @@ -0,0 +1,20 @@ +.base-wrapper { + display: flex; + flex-direction: column; + min-height: 100vh; + width: 100vw; +} + +.base-wrapper__content { + flex: 1; + padding: 16px; + background-color: #f5f5f5; +} + +.base-wrapper__footer { + padding: 16px; + background-color: #1976d2; + color: white; + text-align: center; + font-size: 14px; +} diff --git a/apps/web/src/components/BaseComponents/BaseWrapper.tsx b/apps/web/src/components/BaseComponents/BaseWrapper.tsx new file mode 100644 index 0000000..70c32ce --- /dev/null +++ b/apps/web/src/components/BaseComponents/BaseWrapper.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { Header, HeaderProps } from "../Header/Header"; +import { Footer } from "../Footer/Footer"; +import "./BaseWrapper.css"; + +export interface BaseWrapperProps { + /** Header configuration */ + headerProps: HeaderProps; + /** Main content of the page */ + children: React.ReactNode; +} + +const BaseWrapper: React.FC = ({ headerProps, children }) => { + return ( +
+
+
{children}
+
+
+ ); +}; + +export { BaseWrapper }; diff --git a/apps/web/src/components/Checkbox/Checkbox.css b/apps/web/src/components/Checkbox/Checkbox.css index 9fdceda..c8e45a2 100644 --- a/apps/web/src/components/Checkbox/Checkbox.css +++ b/apps/web/src/components/Checkbox/Checkbox.css @@ -1 +1,51 @@ -/* Base Styles */ +.checkbox { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.checkbox--disabled { + cursor: not-allowed; + opacity: 0.6; +} + +.checkbox__input { + display: none; +} + +.checkbox__custom { + width: 20px; + height: 20px; + border: 2px solid #1976d2; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: + background-color 0.2s, + border-color 0.2s; +} + +.checkbox__input:checked + .checkbox__custom { + background-color: #1976d2; + border-color: #1976d2; +} + +.checkbox__custom::after { + content: ""; + width: 12px; + height: 12px; + background-color: #fff; + border-radius: 2px; + display: none; +} + +.checkbox__input:checked + .checkbox__custom::after { + display: block; +} + +.checkbox__label { + font-size: 16px; + color: #333; +} diff --git a/apps/web/src/components/Checkbox/Checkbox.stories.ts b/apps/web/src/components/Checkbox/Checkbox.stories.ts deleted file mode 100644 index 22111c0..0000000 --- a/apps/web/src/components/Checkbox/Checkbox.stories.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { Checkbox } from "./Checkbox"; - -const meta: Meta = { - title: "Components/Form/Checkbox", - component: Checkbox, - parameters: { - layout: "centered", - }, - tags: ["autodocs"], - argTypes: {}, - args: {}, -}; - -export default meta; - -type Story = StoryObj; -export const Main: Story = {}; diff --git a/apps/web/src/components/Checkbox/Checkbox.stories.tsx b/apps/web/src/components/Checkbox/Checkbox.stories.tsx new file mode 100644 index 0000000..e52ad17 --- /dev/null +++ b/apps/web/src/components/Checkbox/Checkbox.stories.tsx @@ -0,0 +1,42 @@ +import React, { useState } from "react"; +import { Meta, StoryFn } from "@storybook/react"; +import { Checkbox, CheckboxProps } from "./Checkbox"; + +const meta: Meta = { + title: "Components/Checkbox", + component: Checkbox, + argTypes: { + label: { control: "text" }, + checked: { control: "boolean" }, + disabled: { control: "boolean" }, + }, +}; + +export default meta; + +const Template: StoryFn = (args) => { + const [checked, setChecked] = useState(args.checked); + + return ; +}; + +export const Default = Template.bind({}); +Default.args = { + label: "Mercedes Benz", + checked: false, + disabled: false, +}; + +export const Checked = Template.bind({}); +Checked.args = { + label: "Mercedes Benz", + checked: true, + disabled: false, +}; + +export const Disabled = Template.bind({}); +Disabled.args = { + label: "Mercedes Benz", + checked: false, + disabled: true, +}; diff --git a/apps/web/src/components/Checkbox/Checkbox.tsx b/apps/web/src/components/Checkbox/Checkbox.tsx index a496203..3a770de 100644 --- a/apps/web/src/components/Checkbox/Checkbox.tsx +++ b/apps/web/src/components/Checkbox/Checkbox.tsx @@ -1,11 +1,36 @@ import React from "react"; import "./Checkbox.css"; -export interface CheckboxProps extends React.HTMLAttributes {} +export interface CheckboxProps { + /** Label for the checkbox */ + label: string; + /** Whether the checkbox is checked */ + checked: boolean; + /** Disabled state */ + disabled?: boolean; + /** Callback when the checkbox is toggled */ + onChange: (checked: boolean) => void; +} -const Checkbox = ({ ...props }: CheckboxProps) => { - const classNames = []; - return
; +const Checkbox: React.FC = ({ + label, + checked, + disabled = false, + onChange, +}) => { + return ( + + ); }; export { Checkbox }; diff --git a/apps/web/src/components/DropDown/DropDown.css b/apps/web/src/components/DropDown/DropDown.css index 9fdceda..d5f7ed6 100644 --- a/apps/web/src/components/DropDown/DropDown.css +++ b/apps/web/src/components/DropDown/DropDown.css @@ -1 +1,93 @@ -/* Base Styles */ +/* Container */ +.dropdown { + position: relative; + display: flex; + flex-direction: column; +} + +/* Dropdown control (input + button) */ +.dropdown__control { + display: flex; + align-items: center; + border: 1px solid #ccc; + border-radius: 8px; + padding: 8px 12px; + background-color: #fff; + cursor: pointer; + transition: border-color 0.2s ease-in-out; +} + +.dropdown--error .dropdown__control { + border-color: #e53935; +} + +.dropdown__control:focus-within { + border-color: #1976d2; +} + +/* Input field */ +.dropdown__input { + flex-grow: 1; + border: none; + outline: none; + font-size: 14px; + color: #333; + background-color: transparent; + padding: 0; +} + +/* Dropdown button */ +.dropdown__button { + background: none; + border: none; + font-size: 16px; + color: #999; + cursor: pointer; +} + +/* Options container */ +.dropdown__options { + position: absolute; + top: 100%; + left: 0; + right: 0; + margin-top: 4px; + max-height: 200px; + overflow-y: auto; + background-color: #fff; + border: 1px solid #ccc; + border-radius: 8px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); + z-index: 1000; +} + +/* Option styles */ +.dropdown__option { + padding: 8px 12px; + font-size: 14px; + cursor: pointer; + transition: background-color 0.2s ease-in-out; +} + +.dropdown__option:hover { + background-color: #f0f0f0; +} + +.dropdown__empty { + padding: 8px 12px; + font-size: 14px; + color: #999; + text-align: center; +} + +/* Selected option styling */ +.dropdown__option--selected { + background-color: #e0f7fa; + color: #00796b; +} + +/* Checkbox styling */ +.dropdown__option input[type="checkbox"] { + margin-right: 8px; + cursor: pointer; +} diff --git a/apps/web/src/components/DropDown/DropDown.stories.ts b/apps/web/src/components/DropDown/DropDown.stories.ts deleted file mode 100644 index ee20117..0000000 --- a/apps/web/src/components/DropDown/DropDown.stories.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { DropDown } from "./DropDown"; - -const meta: Meta = { - title: "Components/Form/DropDown", - component: DropDown, - parameters: { - layout: "centered", - }, - tags: ["autodocs"], - argTypes: {}, - args: {}, -}; - -export default meta; - -type Story = StoryObj; -export const Main: Story = {}; diff --git a/apps/web/src/components/DropDown/DropDown.stories.tsx b/apps/web/src/components/DropDown/DropDown.stories.tsx new file mode 100644 index 0000000..4ad7dff --- /dev/null +++ b/apps/web/src/components/DropDown/DropDown.stories.tsx @@ -0,0 +1,75 @@ +import React, { useState } from "react"; +import { Meta, StoryFn } from "@storybook/react"; +import { DropDown, DropDownProps } from "./DropDown"; + +const meta: Meta = { + title: "Components/DropDown", + component: DropDown, + argTypes: { + options: { + control: { + type: "object", + }, + defaultValue: [ + { id: 1, label: "گزینه اول" }, + { id: 2, label: "گزینه دوم" }, + { id: 3, label: "گزینه سوم" }, + { id: 4, label: "گزینه چهارم" }, + ], + }, + placeholder: { control: "text" }, + error: { control: "boolean" }, + multi: { control: "boolean" }, + }, +}; + +export default meta; + +const Template: StoryFn = (args) => { + const [selected, setSelected] = useState<{ id: number; label: string }[]>( + args.value || [], + ); + + return ; +}; + +export const Default = Template.bind({}); +Default.args = { + options: [ + { id: 1, label: "گزینه اول" }, + { id: 2, label: "گزینه دوم" }, + { id: 3, label: "گزینه سوم" }, + { id: 4, label: "گزینه چهارم" }, + ], + placeholder: "انتخاب کنید", + value: [], + multi: false, + error: false, +}; + +export const MultiSelect = Template.bind({}); +MultiSelect.args = { + options: [ + { id: 1, label: "گزینه اول" }, + { id: 2, label: "گزینه دوم" }, + { id: 3, label: "گزینه سوم" }, + { id: 4, label: "گزینه چهارم" }, + ], + placeholder: "چند مورد انتخاب کنید", + value: [], + multi: true, + error: false, +}; + +export const WithError = Template.bind({}); +WithError.args = { + options: [ + { id: 1, label: "گزینه اول" }, + { id: 2, label: "گزینه دوم" }, + { id: 3, label: "گزینه سوم" }, + ], + placeholder: "انتخاب کنید", + value: [], + multi: false, + error: true, +}; diff --git a/apps/web/src/components/DropDown/DropDown.tsx b/apps/web/src/components/DropDown/DropDown.tsx index 5764184..bc66ff3 100644 --- a/apps/web/src/components/DropDown/DropDown.tsx +++ b/apps/web/src/components/DropDown/DropDown.tsx @@ -1,11 +1,123 @@ -import React from "react"; +import React, { useState, useRef, useEffect } from "react"; import "./DropDown.css"; -export interface DropDownProps extends React.HTMLAttributes {} +export interface DropDownProps { + /** Options to display in the dropdown */ + options?: { id: number; label: string }[]; + /** Placeholder text for the dropdown */ + placeholder?: string; + /** Selected options (array for multi-select) */ + value: { id: number; label: string }[]; + /** Handler when the selection changes */ + onChange: (selected: { id: number; label: string }[]) => void; + /** Error state */ + error?: boolean; + /** Enable multi-select */ + multi?: boolean; +} -const DropDown = ({ ...props }: DropDownProps) => { - const classNames = []; - return
; +const DropDown: React.FC = ({ + options = [], + placeholder = "انتخاب کنید", + value, + onChange, + error = false, + multi = false, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [query, setQuery] = useState(""); + const dropdownRef = useRef(null); + + // Filtered options based on the query + const filteredOptions = + options?.filter((option) => + option.label.toLowerCase().includes(query.toLowerCase()), + ) || []; + + // Handle click outside dropdown + useEffect(() => { + const handleOutsideClick = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + document.addEventListener("mousedown", handleOutsideClick); + return () => document.removeEventListener("mousedown", handleOutsideClick); + }, []); + + const handleOptionSelect = (option: { id: number; label: string }) => { + if (multi) { + const alreadySelected = value.some( + (selected) => selected.id === option.id, + ); + const newSelection = alreadySelected + ? value.filter((selected) => selected.id !== option.id) // Remove if already selected + : [...value, option]; // Add if not selected + onChange(newSelection); + } else { + onChange([option]); // Single selection + setIsOpen(false); + } + }; + + const isSelected = (option: { id: number; label: string }) => + value.some((selected) => selected.id === option.id); + + return ( +
+
setIsOpen((prev) => !prev)} + > + v.label).join(", ") : value[0]?.label || "" + } + onChange={(e) => setQuery(e.target.value)} + onFocus={() => setIsOpen(true)} + readOnly={!multi} // Make input readonly for multi-select + /> + +
+ {isOpen && ( +
    + {filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( +
  • handleOptionSelect(option)} + > + {multi && ( + + )} + {option.label} +
  • + )) + ) : ( +
  • هیچ موردی پیدا نشد
  • + )} +
+ )} +
+ ); }; export { DropDown }; diff --git a/apps/web/src/components/Footer/Footer.css b/apps/web/src/components/Footer/Footer.css index 9fdceda..40cb438 100644 --- a/apps/web/src/components/Footer/Footer.css +++ b/apps/web/src/components/Footer/Footer.css @@ -1 +1,163 @@ -/* Base Styles */ +.footer { + width: 100%; + max-width: 1280px; + /* */ + background-color: #1e1e1e; + color: #fff; + box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.35); + border-radius: 16px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 56px; + gap: 16px; + /* */ + margin: 0 auto; + margin-bottom: 32px; +} + +.footer__top { + display: flex; + justify-content: space-between; + width: 100%; +} + +.footer__info { + width: 100%; + border: 1px solid #868686; + border-radius: 16px; + padding: 16px; + display: flex; + flex-direction: row; + justify-content: space-around; + align-items: center; + gap: 32px; +} + +.footer__info-item { + display: flex; + flex-direction: row-reverse; + align-items: center; + text-align: center; +} + +.footer__info-content { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.footer__info-title { + font-size: 16px; + font-weight: bold; +} + +.footer__info-text { + font-size: 14px; +} + +.footer__info-icon { + font-size: 32px; + width: 40px; + height: 40px; + border: 1px solid #868686aa; + border-radius: 16px; + padding: 8px; + margin-left: 8px; +} + +.footer__middle { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 24px 64px; +} + +.footer__about { + flex: 1; + font-size: 14px; + line-height: 1.5; +} + +.footer__logo { + font-size: 20px; + font-weight: bold; + display: flex; + justify-content: start; + align-items: center; + justify-items: start; +} + +.footer__logo_svg { + width: 92px; + height: 92px; +} + +.footer__links { + flex: 1; +} + +.footer__link-title { + font-size: 16px; + font-weight: bold; +} + +.footer__links ul { + list-style: none; + padding: 0; + margin: 8px 0 0 0; +} + +.footer__links li { + font-size: 14px; + margin-top: 8px; +} + +.footer__newsletter { + flex: 1; + display: flex; + flex-direction: column; +} + +.footer__newsletter-title { + font-size: 16px; + font-weight: bold; + margin-bottom: 8px; +} + +.footer__newsletter-form { + display: flex; + gap: 8px; +} + +.footer__newsletter-form input { + flex: 1; + padding: 8px; + font-size: 14px; + border: none; + border-radius: 4px; +} + +.footer__newsletter-form button { + padding: 8px 16px; + background-color: #fdb713; + border: none; + border-radius: 4px; + font-size: 14px; + color: #1e1e1e; + cursor: pointer; +} + +.footer__newsletter-form button:hover { + background-color: #e0a602; +} + +.footer__bottom { + width: 100%; + text-align: center; + padding-top: 16px; + border-top: 1px solid rgba(255, 255, 255, 0.2); + font-size: 12px; +} diff --git a/apps/web/src/components/Footer/Footer.tsx b/apps/web/src/components/Footer/Footer.tsx index 71fe202..e79834b 100644 --- a/apps/web/src/components/Footer/Footer.tsx +++ b/apps/web/src/components/Footer/Footer.tsx @@ -1,11 +1,82 @@ import React from "react"; import "./Footer.css"; +import { Logo } from "../Logo/Logo"; -export interface FooterProps extends React.HTMLAttributes {} +const Footer: React.FC = () => { + const [newsletterEmail, setNewsletterEmail] = React.useState(""); -const Footer = ({ ...props }: FooterProps) => { - const classNames = []; - return
; + const handleSubscribe = () => { + if (newsletterEmail.trim()) { + alert(`Subscribed with: ${newsletterEmail}`); + setNewsletterEmail(""); + } + }; + + return ( +
+
+
+
+
+ ارتباط با ما + 0912-2123456 +
+ 📞 +
+
+
+ ایمیل + autorent@info.com +
+ 📧 +
+
+
+ آدرس + تهران - خ شادمان +
+ 📍 +
+
+
+
+
+
+ + رنت‌منت! +
+

+ رنت‌منت با رویکرد اعتماد به مشتری با در اختیار داشتن بزرگترین ناوگان + خودروئی شامل از انواع خودروهای سفر کوتاه، اقتصادی تا تجاری در سراسر + کشور ایران آماده خدمت‌رسانی به مشتریان است. +

+
+
+ دسترسی آسان +
    +
  • سوالات متداول
  • +
  • تماس با ما
  • +
  • درباره ما
  • +
+
+
+ خبرنامه +
+ setNewsletterEmail(e.target.value)} + /> + +
+
+
+
+

تمامی حقوق این وبسایت متعلق به رنت‌منت می‌باشد ©

+
+
+ ); }; export { Footer }; diff --git a/apps/web/src/components/Header/Header.stories.ts b/apps/web/src/components/Header/Header.stories.ts deleted file mode 100644 index 28e09fd..0000000 --- a/apps/web/src/components/Header/Header.stories.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { fn } from "@storybook/test"; - -import { Header } from "./Header"; - -const meta: Meta = { - title: "Sections/Header", - component: Header, - // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs - tags: ["autodocs"], - parameters: { - // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout - layout: "fullscreen", - }, - args: { - onLogin: fn(), - onLogout: fn(), - onCreateAccount: fn(), - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const LoggedIn: Story = { - args: { - user: { - name: "Jane Doe", - }, - }, -}; - -export const LoggedOut: Story = {}; diff --git a/apps/web/src/components/Header/Header.stories.tsx b/apps/web/src/components/Header/Header.stories.tsx new file mode 100644 index 0000000..f94f131 --- /dev/null +++ b/apps/web/src/components/Header/Header.stories.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { Meta, StoryFn } from "@storybook/react"; +import { Header, HeaderProps } from "./Header"; + +const meta: Meta = { + title: "Components/Header", + component: Header, + argTypes: { + navLinks: { control: "object" }, + user: { control: "object" }, + onSearchClick: { action: "Search clicked" }, + onLogin: { action: "Login clicked" }, + onLogout: { action: "Logout clicked" }, + onCreateAccount: { action: "Create account clicked" }, + }, +}; + +export default meta; + +const Template: StoryFn = (args) =>
; + +export const LoggedIn = Template.bind({}); +LoggedIn.args = { + navLinks: [ + { label: "رزرو خودرو", href: "#" }, + { label: "خدمات ما", href: "#" }, + { label: "بلاگ", href: "#" }, + { label: "درباره ما", href: "#" }, + { label: "تماس با ما", href: "#" }, + ], + user: { + name: "اولدوز بهاور", + avatarUrl: "https://via.placeholder.com/32", + }, +}; + +export const LoggedOut = Template.bind({}); +LoggedOut.args = { + navLinks: [ + { label: "رزرو خودرو", href: "#" }, + { label: "خدمات ما", href: "#" }, + { label: "بلاگ", href: "#" }, + { label: "درباره ما", href: "#" }, + { label: "تماس با ما", href: "#" }, + ], +}; diff --git a/apps/web/src/components/Header/Header.tsx b/apps/web/src/components/Header/Header.tsx index 7253b9e..97774ce 100644 --- a/apps/web/src/components/Header/Header.tsx +++ b/apps/web/src/components/Header/Header.tsx @@ -1,68 +1,82 @@ import React from "react"; -import { Button } from "../Button/Button"; -import "./header.css"; - -type User = { - name: string; -}; +import { Logo } from "../Logo/Logo"; +import "./Header.css"; export interface HeaderProps { - user?: User; + /** Navigation links */ + navLinks: { label: string; href: string }[]; + /** Logged-in user data */ + user?: { + name: string; + avatarUrl: string; + }; + /** Search icon or search functionality */ + onSearchClick?: () => void; + /** Callback when login button is clicked */ onLogin?: () => void; + /** Callback when logout button is clicked */ onLogout?: () => void; + /** Callback when create account button is clicked */ onCreateAccount?: () => void; } -const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => ( -
-
-
- - - - - - - -

Acme

+const Header: React.FC = ({ + navLinks, + user, + onSearchClick, + onLogin, + onLogout, + onCreateAccount, +}) => { + return ( +
+
+ + رنت‌منت!
-
+ +
{user ? ( - <> - - Welcome, {user.name}! - - */} +
) : ( - <> - + +
)}
-
-
-); +
+ ); +}; export { Header }; diff --git a/apps/web/src/components/Header/header.css b/apps/web/src/components/Header/header.css index ad77492..b7200b7 100644 --- a/apps/web/src/components/Header/header.css +++ b/apps/web/src/components/Header/header.css @@ -1,32 +1,124 @@ -.storybook-header { +.header { + /* */ + height: 4rem; + width: 100%; + max-width: 1280px; + margin: 0 auto; + margin-bottom: 1rem; + border-radius: 0px 0px 16px 16px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + /* */ display: flex; justify-content: space-between; align-items: center; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); - padding: 15px 20px; - font-family: "Nunito Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + background-color: #fff; + padding: 16px 32px; } -.storybook-header svg { - display: inline-block; - vertical-align: top; +.header__logo { + font-size: 20px; + font-weight: bold; + display: flex; + justify-content: end; + align-items: center; + justify-items: end; } -.storybook-header h1 { - display: inline-block; - vertical-align: top; - margin: 6px 0 6px 10px; - font-weight: 700; - font-size: 20px; - line-height: 1; +.header__logo_svg { + width: 92px; + height: 92px; + display: flex; + flex-direction: row; +} + +.header__nav { + display: flex; + gap: 32px; +} + +.header__link { + text-decoration: none; + color: #333; + font-size: 16px; + font-weight: 500; + transition: color 0.2s; + display: inline-flex; + align-items: center; } -.storybook-header button + button { - margin-left: 10px; +.header__link:hover { + color: #1976d2; +} + +.header__actions { + display: flex; + align-items: center; + gap: 16px; } -.storybook-header .welcome { - margin-right: 10px; +.header__search { + background: none; + border: none; + font-size: 18px; + cursor: pointer; color: #333; +} + +.header__search:hover { + color: #1976d2; +} + +.header__user { + display: flex; + align-items: center; + gap: 8px; +} + +.header__avatar { + width: 40px; + height: 40px; + border-radius: 50%; +} + +.header__username { + font-size: 16px; + color: #333; +} + +/* Add styles for the new buttons */ +.header__auth-buttons { + display: flex; + gap: 8px; +} + +.header__login, +.header__create-account { + background-color: #1976d2; + color: #fff; + border: none; + border-radius: 4px; + padding: 8px 16px; font-size: 14px; + cursor: pointer; + transition: background-color 0.2s ease-in-out; +} + +.header__login:hover, +.header__create-account:hover { + background-color: #155a99; +} + +.header__logout { + background-color: #d32f2f; + color: #fff; + border: none; + border-radius: 4px; + padding: 8px 16px; + font-size: 14px; + cursor: pointer; + transition: background-color 0.2s ease-in-out; +} + +.header__logout:hover { + background-color: #b71c1c; } diff --git a/apps/web/src/components/HelperText/HelperText.css b/apps/web/src/components/HelperText/HelperText.css index 9fdceda..fdd1c5f 100644 --- a/apps/web/src/components/HelperText/HelperText.css +++ b/apps/web/src/components/HelperText/HelperText.css @@ -1 +1,31 @@ -/* Base Styles */ +/* Base styles for HelperText */ +.helper-text { + display: flex; + align-items: center; + gap: 4px; + font-size: 14px; + line-height: 1.5; + direction: rtl; /* Assuming Persian/Arabic text direction */ +} + +.helper-text__icon { + display: inline-block; +} + +.helper-text__text { + display: inline-block; +} + +/* Variant styles */ +.helper-text--info { + color: #6c757d; /* Gray color for info */ +} + +.helper-text--error { + color: #dc3545; /* Red color for error */ +} + +/* Disabled styles */ +.helper-text--disabled { + color: #adb5bd; /* Lighter gray for disabled text */ +} diff --git a/apps/web/src/components/HelperText/HelperText.stories.ts b/apps/web/src/components/HelperText/HelperText.stories.ts deleted file mode 100644 index 804628b..0000000 --- a/apps/web/src/components/HelperText/HelperText.stories.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { HelperText } from "./HelperText"; - -const meta: Meta = { - title: "Components/Form/HelperText", - component: HelperText, - parameters: { - layout: "centered", - }, - tags: ["autodocs"], - argTypes: {}, - args: {}, -}; - -export default meta; - -type Story = StoryObj; -export const Main: Story = {}; diff --git a/apps/web/src/components/HelperText/HelperText.stories.tsx b/apps/web/src/components/HelperText/HelperText.stories.tsx new file mode 100644 index 0000000..9ebc760 --- /dev/null +++ b/apps/web/src/components/HelperText/HelperText.stories.tsx @@ -0,0 +1,47 @@ +import React, { useState } from "react"; +import { Meta, StoryFn } from "@storybook/react"; +import { HelperText, HelperTextProps } from "./HelperText"; + +const meta: Meta = { + title: "Components/HelperText", + component: HelperText, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: { + variant: { + control: { + type: "select", + options: ["info", "error"], + }, + }, + disabled: { control: "boolean" }, + icon: { control: { type: "text" } }, + }, +}; + +export default meta; + +const Template: StoryFn = (args) => ; + +export const Info: StoryFn = Template.bind({}); +Info.args = { + text: "متن راهنما", + variant: "info", +}; + +export const Error: StoryFn = Template.bind({}); +Error.args = { + text: "متن راهنما", + variant: "error", + icon: "❗", +}; + +export const Disabled: StoryFn = Template.bind({}); +Disabled.args = { + text: "متن راهنما", + variant: "info", + disabled: true, + icon: "❗", +}; diff --git a/apps/web/src/components/HelperText/HelperText.tsx b/apps/web/src/components/HelperText/HelperText.tsx index ce2dae7..94fecf8 100644 --- a/apps/web/src/components/HelperText/HelperText.tsx +++ b/apps/web/src/components/HelperText/HelperText.tsx @@ -1,11 +1,35 @@ import React from "react"; import "./HelperText.css"; -export interface HelperTextProps extends React.HTMLAttributes {} +export interface HelperTextProps { + /** Text to display */ + text: string; + /** Variant of the helper text */ + variant?: "info" | "error"; + /** Optional icon */ + icon?: React.ReactNode; + /** Whether the text is disabled */ + disabled?: boolean; +} -const HelperText = ({ ...props }: HelperTextProps) => { - const classNames = []; - return
; +const HelperText: React.FC = ({ + text, + variant = "info", + icon = null, + disabled = false, +}) => { + const classNames = [ + "helper-text", + `helper-text--${variant}`, + disabled ? "helper-text--disabled" : "", + ].join(" "); + + return ( +
+ {icon && {icon}} + {text} +
+ ); }; export { HelperText }; diff --git a/apps/web/src/components/Input/Input.css b/apps/web/src/components/Input/Input.css deleted file mode 100644 index 9fdceda..0000000 --- a/apps/web/src/components/Input/Input.css +++ /dev/null @@ -1 +0,0 @@ -/* Base Styles */ diff --git a/apps/web/src/components/Input/Input.stories.ts b/apps/web/src/components/Input/Input.stories.ts deleted file mode 100644 index 3afe352..0000000 --- a/apps/web/src/components/Input/Input.stories.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { Input } from "./Input"; - -const meta: Meta = { - title: "Components/Form/Input", - component: Input, - parameters: { - layout: "centered", - }, - tags: ["autodocs"], - argTypes: {}, - args: {}, -}; - -export default meta; - -type Story = StoryObj; -export const Main: Story = {}; diff --git a/apps/web/src/components/Input/Input.tsx b/apps/web/src/components/Input/Input.tsx deleted file mode 100644 index be6baa4..0000000 --- a/apps/web/src/components/Input/Input.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; -import "./Input.css"; - -export interface InputProps extends React.HTMLAttributes {} - -const Input = ({ ...props }: InputProps) => { - const classNames = []; - return
; -}; - -export { Input }; diff --git a/apps/web/src/components/Input/TextInput.css b/apps/web/src/components/Input/TextInput.css new file mode 100644 index 0000000..adc0073 --- /dev/null +++ b/apps/web/src/components/Input/TextInput.css @@ -0,0 +1,72 @@ +/* Container styles */ +.text-input { + display: flex; + flex-direction: column; + gap: 4px; +} + +.text-input-label { + font-size: 14px; + font-weight: 500; + color: #333; +} + +.text-input-wrapper { + display: flex; + align-items: center; + gap: 8px; + border: 1px solid #ccc; + border-radius: 8px; + padding: 8px; + transition: border-color 0.2s ease-in-out; +} + +.text-input-field { + flex-grow: 1; + border: none; + outline: none; + font-size: 14px; +} + +.text-input-field[type="number"] { + direction: ltr; +} + +.icon-left, +.icon-right, +.toggle-password-visibility { + font-size: 16px; + color: #999; + background: none; + border: none; + cursor: pointer; +} + +.toggle-password-visibility:hover { + color: #333; +} + +.clear-button { + background: none; + border: none; + cursor: pointer; + font-size: 16px; + color: #999; +} + +.clear-button:hover { + color: #333; +} + +.error { + border-color: #e53935; +} + +/* Focus and hover states */ +.text-input-wrapper:hover { + border-color: #1976d2; +} + +.text-input-wrapper:focus-within { + border-color: #1976d2; +} diff --git a/apps/web/src/components/Input/TextInput.stories.tsx b/apps/web/src/components/Input/TextInput.stories.tsx new file mode 100644 index 0000000..331ff0c --- /dev/null +++ b/apps/web/src/components/Input/TextInput.stories.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { Meta, StoryFn } from "@storybook/react"; +import { TextInput, TextInputProps } from "./TextInput"; + +const meta: Meta = { + title: "Components/TextInput", + component: TextInput, + argTypes: { + type: { + control: { + type: "select", + options: ["text", "number", "email", "password", "tel"], + }, + }, + errorText: { + control: "text", + }, + helperText: { + control: "text", + }, + iconLeft: { control: false }, + iconRight: { control: false }, + }, +}; + +export default meta; + +const Template: StoryFn = (args: TextInputProps) => ( + +); + +export const Default = Template.bind({}); +Default.args = { + label: "عنوان", + type: "text", + placeholder: "متن ورودی", + helperText: "متن راهنما", + required: true, +}; + +export const PasswordWithToggle = Template.bind({}); +PasswordWithToggle.args = { + label: "رمز عبور", + type: "password", + placeholder: "••••••••", + helperText: "رمز عبور خود را وارد کنید", + required: true, +}; + +export const WithError = Template.bind({}); +WithError.args = { + ...Default.args, + errorText: "متن خطا", +}; diff --git a/apps/web/src/components/Input/TextInput.tsx b/apps/web/src/components/Input/TextInput.tsx new file mode 100644 index 0000000..87a62fd --- /dev/null +++ b/apps/web/src/components/Input/TextInput.tsx @@ -0,0 +1,99 @@ +import React, { useState } from "react"; +import { HelperText } from "../HelperText/HelperText"; +import "./TextInput.css"; + +export interface TextInputProps + extends React.InputHTMLAttributes { + label: string; + type?: "text" | "number" | "email" | "password" | "tel"; + placeholder?: string; + value?: string; + errorText?: string; + helperText?: string; + required?: boolean; + iconLeft?: React.ReactNode; + iconRight?: React.ReactNode; + onClear?: () => void; +} + +const TextInput = ({ + label, + type = "text", + placeholder = "", + value = "", + errorText = "", + helperText = "", + required = false, + iconLeft = null, + iconRight = null, + onClear, + ...props +}: TextInputProps) => { + const [inputValue, setInputValue] = useState(value); + const [isPasswordVisible, setIsPasswordVisible] = useState(false); + + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + if (props.onChange) props.onChange(e); + }; + + const handleClear = () => { + setInputValue(""); + if (onClear) onClear(); + }; + + const togglePasswordVisibility = () => { + setIsPasswordVisible((prev) => !prev); + }; + + const renderRightIcon = () => { + if (type === "password") { + return ( + + ); + } + if (iconRight) { + return {iconRight}; + } + return null; + }; + + return ( +
+ +
+ {iconLeft && {iconLeft}} + + {inputValue && onClear && ( + + )} + {renderRightIcon()} +
+ {errorText ? ( + + ) : ( + helperText && + )} +
+ ); +}; + +export { TextInput }; diff --git a/apps/web/src/components/Logo/Logo.css b/apps/web/src/components/Logo/Logo.css deleted file mode 100644 index 9fdceda..0000000 --- a/apps/web/src/components/Logo/Logo.css +++ /dev/null @@ -1 +0,0 @@ -/* Base Styles */ diff --git a/apps/web/src/components/Logo/Logo.tsx b/apps/web/src/components/Logo/Logo.tsx index 1fe3432..59b1a35 100644 --- a/apps/web/src/components/Logo/Logo.tsx +++ b/apps/web/src/components/Logo/Logo.tsx @@ -1,11 +1,48 @@ import React from "react"; -import "./Logo.css"; -export interface LogoProps extends React.HTMLAttributes {} +export interface LogoProps { + /** Width of the logo */ + width?: number; + /** Height of the logo */ + height?: number; + /** Optional additional class names for styling */ + className?: string; +} -const Logo = ({ ...props }: LogoProps) => { - const classNames = []; - return
; +const Logo: React.FC = ({ + width = "100%", + height = "100%", + className, +}) => { + return ( + + {/* Outer Circle */} + + {/* Inner Circle */} + + {/* Pointer */} + + {/* Text */} + + رنت‌منت + + + ); }; export { Logo }; diff --git a/apps/web/src/components/OPTInput/OPTInput.css b/apps/web/src/components/OPTInput/OPTInput.css index 9fdceda..31722a7 100644 --- a/apps/web/src/components/OPTInput/OPTInput.css +++ b/apps/web/src/components/OPTInput/OPTInput.css @@ -1 +1,33 @@ -/* Base Styles */ +/* Container for the OTP input */ +.opt-input { + display: flex; + gap: 8px; + justify-content: center; + direction: ltr; +} + +/* Single input box styles */ +.opt-input__box { + width: 48px; + height: 48px; + border: 1px solid #ccc; + border-radius: 8px; + text-align: center; + font-size: 18px; + font-weight: bold; + outline: none; + transition: border-color 0.2s ease-in-out; +} + +.opt-input__box:focus { + border-color: #1976d2; +} + +.opt-input--error .opt-input__box { + border-color: #e53935; +} + +/* Error styles for the container */ +.opt-input--error .opt-input__box:focus { + border-color: #e53935; +} diff --git a/apps/web/src/components/OPTInput/OPTInput.stories.ts b/apps/web/src/components/OPTInput/OPTInput.stories.ts deleted file mode 100644 index 5d3db10..0000000 --- a/apps/web/src/components/OPTInput/OPTInput.stories.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { OPTInput } from "./OPTInput"; - -const meta: Meta = { - title: "Components/Form/OPTInput", - component: OPTInput, - parameters: { - layout: "centered", - }, - tags: ["autodocs"], - argTypes: {}, - args: {}, -}; - -export default meta; - -type Story = StoryObj; -export const Main: Story = {}; diff --git a/apps/web/src/components/OPTInput/OPTInput.stories.tsx b/apps/web/src/components/OPTInput/OPTInput.stories.tsx new file mode 100644 index 0000000..059c892 --- /dev/null +++ b/apps/web/src/components/OPTInput/OPTInput.stories.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Meta, StoryFn } from "@storybook/react"; +import { OPTInput, OPTInputProps } from "./OPTInput"; + +const meta: Meta = { + title: "Components/OPTInput", + component: OPTInput, + argTypes: { + length: { control: "number" }, + error: { control: "boolean" }, + onComplete: { action: "completed" }, + }, +}; + +export default meta; + +const Template: StoryFn = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + length: 5, + error: false, +}; + +export const ErrorState = Template.bind({}); +ErrorState.args = { + length: 5, + error: true, +}; diff --git a/apps/web/src/components/OPTInput/OPTInput.tsx b/apps/web/src/components/OPTInput/OPTInput.tsx index be0034a..1253318 100644 --- a/apps/web/src/components/OPTInput/OPTInput.tsx +++ b/apps/web/src/components/OPTInput/OPTInput.tsx @@ -1,11 +1,82 @@ -import React from "react"; +import React, { useRef, useState } from "react"; import "./OPTInput.css"; -export interface OPTInputProps extends React.HTMLAttributes {} +export interface OPTInputProps { + /** Number of input boxes */ + length: number; + /** Callback function when all inputs are filled */ + onComplete: (code: string) => void; + /** Error state for invalid input */ + error?: boolean; +} -const OPTInput = ({ ...props }: OPTInputProps) => { - const classNames = []; - return
; +const OPTInput: React.FC = ({ + length, + onComplete, + error = false, +}) => { + const [values, setValues] = useState(Array(length).fill("")); + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + + const handleChange = (value: string, index: number) => { + if (!/^\d?$/.test(value)) return; // Allow only digits + const newValues = [...values]; + newValues[index] = value; + setValues(newValues); + + if (value && index < length - 1) { + inputRefs.current[index + 1]?.focus(); + } + + if (newValues.every((val) => val !== "")) { + onComplete(newValues.join("")); + } + }; + + const handleBackspace = (value: string, index: number) => { + if (!value && index > 0) { + inputRefs.current[index - 1]?.focus(); + } + }; + + const handlePaste = (e: React.ClipboardEvent) => { + const pasteData = e.clipboardData.getData("text").slice(0, length); + if (!/^\d+$/.test(pasteData)) return; + + const newValues = pasteData.split("").slice(0, length); + setValues(newValues); + + newValues.forEach((val, idx) => { + if (inputRefs.current[idx]) { + inputRefs.current[idx]!.value = val; + } + }); + + if (newValues.length === length) { + onComplete(newValues.join("")); + } + }; + + return ( +
+ {Array.from({ length }, (_, index) => ( + (inputRefs.current[index] = el)} + type="text" + maxLength={1} + value={values[index]} + onChange={(e) => handleChange(e.target.value, index)} + onKeyDown={(e) => { + if (e.key === "Backspace") + handleBackspace(e.currentTarget.value, index); + }} + onPaste={handlePaste} + className="opt-input__box" + /> + ))} +
+ ); }; export { OPTInput }; diff --git a/apps/web/src/components/RadioButton/RadioButton.css b/apps/web/src/components/RadioButton/RadioButton.css index 9fdceda..dbffa81 100644 --- a/apps/web/src/components/RadioButton/RadioButton.css +++ b/apps/web/src/components/RadioButton/RadioButton.css @@ -1 +1,51 @@ -/* Base Styles */ +.radio-button { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.radio-button--disabled { + cursor: not-allowed; + opacity: 0.6; +} + +.radio-button__input { + display: none; +} + +.radio-button__custom { + width: 20px; + height: 20px; + border: 2px solid #1976d2; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: + background-color 0.2s, + border-color 0.2s; +} + +.radio-button__input:checked + .radio-button__custom { + background-color: #1976d2; + border-color: #1976d2; +} + +.radio-button__custom::after { + content: ""; + width: 12px; + height: 12px; + background-color: #fff; + border-radius: 50%; + display: none; +} + +.radio-button__input:checked + .radio-button__custom::after { + display: block; +} + +.radio-button__label { + font-size: 16px; + color: #333; +} diff --git a/apps/web/src/components/RadioButton/RadioButton.stories.ts b/apps/web/src/components/RadioButton/RadioButton.stories.ts deleted file mode 100644 index 7666377..0000000 --- a/apps/web/src/components/RadioButton/RadioButton.stories.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { RadioButton } from "./RadioButton"; - -const meta: Meta = { - title: "Components/Form/RadioButton", - component: RadioButton, - parameters: { - layout: "centered", - }, - tags: ["autodocs"], - argTypes: {}, - args: {}, -}; - -export default meta; - -type Story = StoryObj; -export const Main: Story = {}; diff --git a/apps/web/src/components/RadioButton/RadioButton.stories.tsx b/apps/web/src/components/RadioButton/RadioButton.stories.tsx new file mode 100644 index 0000000..50cd31d --- /dev/null +++ b/apps/web/src/components/RadioButton/RadioButton.stories.tsx @@ -0,0 +1,48 @@ +import React, { useState } from "react"; +import { Meta, StoryFn } from "@storybook/react"; +import { RadioButton, RadioButtonProps } from "./RadioButton"; + +const meta: Meta = { + title: "Components/RadioButton", + component: RadioButton, + argTypes: { + label: { control: "text" }, + selected: { control: "boolean" }, + disabled: { control: "boolean" }, + }, +}; + +export default meta; + +const Template: StoryFn = (args) => { + const [selected, setSelected] = useState(args.selected); + + return ( + setSelected(true)} + /> + ); +}; + +export const Default = Template.bind({}); +Default.args = { + label: "Mercedes Benz", + selected: false, + disabled: false, +}; + +export const Selected = Template.bind({}); +Selected.args = { + label: "Mercedes Benz", + selected: true, + disabled: false, +}; + +export const Disabled = Template.bind({}); +Disabled.args = { + label: "Mercedes Benz", + selected: false, + disabled: true, +}; diff --git a/apps/web/src/components/RadioButton/RadioButton.tsx b/apps/web/src/components/RadioButton/RadioButton.tsx index ca9a71f..2a67641 100644 --- a/apps/web/src/components/RadioButton/RadioButton.tsx +++ b/apps/web/src/components/RadioButton/RadioButton.tsx @@ -1,12 +1,38 @@ import React from "react"; import "./RadioButton.css"; -export interface RadioButtonProps - extends React.HTMLAttributes {} +export interface RadioButtonProps { + /** Label for the radio button */ + label: string; + /** Whether the radio button is selected */ + selected: boolean; + /** Disabled state */ + disabled?: boolean; + /** Callback when the radio button is selected */ + onChange: () => void; +} -const RadioButton = ({ ...props }: RadioButtonProps) => { - const classNames = []; - return
; +const RadioButton: React.FC = ({ + label, + selected, + disabled = false, + onChange, +}) => { + return ( + + ); }; export { RadioButton }; diff --git a/apps/web/src/components/SearchInput/SearchInput.css b/apps/web/src/components/SearchInput/SearchInput.css new file mode 100644 index 0000000..1db1dfe --- /dev/null +++ b/apps/web/src/components/SearchInput/SearchInput.css @@ -0,0 +1,17 @@ +/* Reuse styles from TextInput */ +.search-input__clear-button { + background: none; + border: none; + cursor: pointer; + font-size: 16px; + color: #999; +} + +.search-input__clear-button:hover { + color: #333; +} + +.search-input__search-icon { + font-size: 16px; + color: #999; +} diff --git a/apps/web/src/components/SearchInput/SearchInput.stories.tsx b/apps/web/src/components/SearchInput/SearchInput.stories.tsx new file mode 100644 index 0000000..cb56300 --- /dev/null +++ b/apps/web/src/components/SearchInput/SearchInput.stories.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import { Meta, StoryFn } from "@storybook/react"; +import { SearchInput, SearchInputProps } from "./SearchInput"; + +const meta: Meta = { + title: "Components/SearchInput", + component: SearchInput, + argTypes: { + clearable: { + control: "boolean", + }, + placeholder: { + control: "text", + }, + helperText: { + control: "text", + }, + errorText: { + control: "text", + }, + }, +}; + +export default meta; + +const Template: StoryFn = (args: SearchInputProps) => ( + +); + +export const Default = Template.bind({}); +Default.args = { + label: "جستجو", + placeholder: "جستجو", + helperText: "متن راهنما", + clearable: false, +}; + +export const Clearable = Template.bind({}); +Clearable.args = { + label: "جستجو", + placeholder: "جستجو", + helperText: "قابل حذف", + clearable: true, +}; + +export const WithError = Template.bind({}); +WithError.args = { + label: "جستجو", + placeholder: "جستجو", + errorText: "متن خطا", + clearable: true, +}; diff --git a/apps/web/src/components/SearchInput/SearchInput.tsx b/apps/web/src/components/SearchInput/SearchInput.tsx new file mode 100644 index 0000000..ece60f8 --- /dev/null +++ b/apps/web/src/components/SearchInput/SearchInput.tsx @@ -0,0 +1,52 @@ +import React, { useState } from "react"; +import { TextInput, TextInputProps } from "../Input/TextInput"; +import "./SearchInput.css"; + +export interface SearchInputProps + extends Omit { + /** Whether the input is clearable (displays a clear button) */ + clearable?: boolean; +} + +const SearchInput: React.FC = ({ + clearable = false, + ...props +}) => { + const [inputValue, setInputValue] = useState(props.value || ""); + + const handleClear = () => { + setInputValue(""); + if (props.onChange) { + props.onChange({ + target: { value: "" }, + } as React.ChangeEvent); + } + }; + + return ( + { + setInputValue(e.target.value); + if (props.onChange) props.onChange(e); + }} + iconRight={ + clearable && inputValue ? ( + + ) : ( + 🔍 + ) + } + /> + ); +}; + +export { SearchInput }; diff --git a/apps/web/src/components/TextArea/TextArea.css b/apps/web/src/components/TextArea/TextArea.css index 9fdceda..303699f 100644 --- a/apps/web/src/components/TextArea/TextArea.css +++ b/apps/web/src/components/TextArea/TextArea.css @@ -1 +1,40 @@ -/* Base Styles */ +/* Container for the TextArea */ +.textarea-container { + position: relative; + display: flex; + flex-direction: column; + gap: 8px; +} + +/* Base TextArea styles */ +.textarea { + width: 100%; + min-height: 100px; + padding: 12px; + font-size: 14px; + border: 1px solid #ccc; + border-radius: 8px; + resize: both; + /* Allow resizing */ + overflow: auto; + color: #333; + outline: none; +} + +.textarea:focus { + border-color: #194bf0; + box-shadow: 0 0 4px rgba(25, 75, 240, 0.2); +} + +/* Placeholder styling */ +.textarea::placeholder { + color: #aaa; +} + +/* Footer for character count */ +.textarea-footer { + font-size: 12px; + color: #777; + text-align: left; + direction: rtl; +} diff --git a/apps/web/src/components/TextArea/TextArea.stories.ts b/apps/web/src/components/TextArea/TextArea.stories.ts deleted file mode 100644 index c34c7b4..0000000 --- a/apps/web/src/components/TextArea/TextArea.stories.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { TextArea } from "./TextArea"; - -const meta: Meta = { - title: "Components/Form/TextArea", - component: TextArea, - parameters: { - layout: "centered", - }, - tags: ["autodocs"], - argTypes: {}, - args: {}, -}; - -export default meta; - -type Story = StoryObj; -export const Main: Story = {}; diff --git a/apps/web/src/components/TextArea/TextArea.stories.tsx b/apps/web/src/components/TextArea/TextArea.stories.tsx new file mode 100644 index 0000000..244eedc --- /dev/null +++ b/apps/web/src/components/TextArea/TextArea.stories.tsx @@ -0,0 +1,51 @@ +import React, { useState } from "react"; +import type { Meta, StoryFn } from "@storybook/react"; +import { TextArea } from "./TextArea"; + +const meta: Meta = { + title: "Components/TextArea", + component: TextArea, + parameters: { + layout: "centered", + }, + argTypes: { + placeholder: { + control: "text", + }, + maxLength: { + control: "number", + }, + onChange: { action: "changed" }, + value: { + control: false, // Disabled because we handle it dynamically in the controlled example + }, + }, + args: { + placeholder: "نگهدارنده پیش فرض", + maxLength: 20, + value: "", + }, +}; + +export default meta; + +const Template: StoryFn = (args) => { + const [value, setValue] = useState(args.value || ""); + + return ( + ; +const TextArea = ({ + placeholder = "نگهدارنده پیش فرض", + maxLength = 20, + value, + onChange, + ...props +}: TextAreaProps) => { + return ( +
+