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 + 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 && (
+
+ )}
+
+ );
};
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 (
+
+ );
};
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}!
-
-
- >
+
+
+
{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 (
+
+ {isPasswordVisible ? "🙈" : "👀"}
+
+ );
+ }
+ 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 (
+
+ );
};
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 (
+