diff --git a/package-lock.json b/package-lock.json index 17df74f..46ccfb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,9 @@ "@fortawesome/react-fontawesome": "^0.2.2", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.26.0" + "react-router-dom": "^6.26.0", + "react-slick": "^0.30.2", + "slick-carousel": "^1.8.1" }, "devDependencies": { "@chromatic-com/storybook": "^1.6.1", @@ -3647,6 +3649,11 @@ } } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -4028,6 +4035,11 @@ "node": ">= 0.8" } }, + "node_modules/enquire.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz", + "integrity": "sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==" + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -5999,6 +6011,14 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "dependencies": { + "string-convert": "^0.2.0" + } + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -6096,6 +6116,11 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -7322,6 +7347,22 @@ "react-dom": ">=16.8" } }, + "node_modules/react-slick": { + "version": "0.30.2", + "resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.30.2.tgz", + "integrity": "sha512-XvQJi7mRHuiU3b9irsqS9SGIgftIfdV5/tNcURTb5LdIokRA5kIIx3l4rlq2XYHfxcSntXapoRg/GxaVOM1yfg==", + "dependencies": { + "classnames": "^2.2.5", + "enquire.js": "^2.1.6", + "json2mq": "^0.2.0", + "lodash.debounce": "^4.0.8", + "resize-observer-polyfill": "^1.5.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -7473,6 +7514,11 @@ "node": ">=0.10.5" } }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -7828,6 +7874,14 @@ "node": ">=8" } }, + "node_modules/slick-carousel": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz", + "integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==", + "peerDependencies": { + "jquery": ">=1.8.0" + } + }, "node_modules/snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", @@ -7893,6 +7947,11 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", diff --git a/package.json b/package.json index 07300ff..c849c22 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "@fortawesome/react-fontawesome": "^0.2.2", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.26.0" + "react-router-dom": "^6.26.0", + "react-slick": "^0.30.2", + "slick-carousel": "^1.8.1" }, "devDependencies": { "@chromatic-com/storybook": "^1.6.1", diff --git a/src/App.jsx b/src/App.jsx index b3062f1..e563787 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,9 +1,15 @@ +import Container from "./containers/Container"; +import TopDoctorsSection from "./sections/TopDoctorsSection"; + const App = () => { return (
-

- Welcome to KlinicCon App -

+

Welcome to KlinicCon App

+
+ + + +
); }; diff --git a/src/assets/shape.svg b/src/assets/shape.svg new file mode 100644 index 0000000..a099534 --- /dev/null +++ b/src/assets/shape.svg @@ -0,0 +1,422 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/temp/img_01.webp b/src/assets/temp/img_01.webp new file mode 100644 index 0000000..f1cb530 Binary files /dev/null and b/src/assets/temp/img_01.webp differ diff --git a/src/assets/temp/img_02.webp b/src/assets/temp/img_02.webp new file mode 100644 index 0000000..233d227 Binary files /dev/null and b/src/assets/temp/img_02.webp differ diff --git a/src/assets/temp/img_03.webp b/src/assets/temp/img_03.webp new file mode 100644 index 0000000..0841512 Binary files /dev/null and b/src/assets/temp/img_03.webp differ diff --git a/src/assets/temp/img_04.webp b/src/assets/temp/img_04.webp new file mode 100644 index 0000000..040d20a Binary files /dev/null and b/src/assets/temp/img_04.webp differ diff --git a/src/assets/temp/img_05.webp b/src/assets/temp/img_05.webp new file mode 100644 index 0000000..9379540 Binary files /dev/null and b/src/assets/temp/img_05.webp differ diff --git a/src/components/Paragraph.jsx b/src/components/Paragraph.jsx new file mode 100644 index 0000000..74bf774 --- /dev/null +++ b/src/components/Paragraph.jsx @@ -0,0 +1,16 @@ +import PropTypes from "prop-types"; + +const Paragraph = ({ children, className }) => { + return

{children}

; +}; + +Paragraph.propTypes = { + children: PropTypes.node.isRequired, + className: PropTypes.string, +}; + +Paragraph.defaultProps = { + className: "", +}; + +export default Paragraph; diff --git a/src/components/StarRating.jsx b/src/components/StarRating.jsx new file mode 100644 index 0000000..d601bf4 --- /dev/null +++ b/src/components/StarRating.jsx @@ -0,0 +1,22 @@ +import PropTypes from "prop-types"; +import "@fortawesome/fontawesome-free/css/all.css"; + +const StarRating = ({ rating }) => { + const stars = Array(5) + .fill(0) + .map((_, i) => + i < rating ? ( + + ) : ( + + ) + ); + + return
{stars}
; +}; + +StarRating.propTypes = { + rating: PropTypes.number.isRequired, +}; + +export default StarRating; diff --git a/src/containers/Container.jsx b/src/containers/Container.jsx new file mode 100644 index 0000000..e1b2e7d --- /dev/null +++ b/src/containers/Container.jsx @@ -0,0 +1,20 @@ +import PropTypes from "prop-types"; + +const Container = ({ children, className }) => { + return ( +
+ {children} +
+ ); +}; + +Container.propTypes = { + children: PropTypes.node.isRequired, + className: PropTypes.string, +}; + +Container.defaultProps = { + className: "", +}; + +export default Container; diff --git a/src/containers/DoctorSlider.jsx b/src/containers/DoctorSlider.jsx new file mode 100644 index 0000000..e474a52 --- /dev/null +++ b/src/containers/DoctorSlider.jsx @@ -0,0 +1,72 @@ +import Slider from "react-slick"; +import "slick-carousel/slick/slick.css"; +import "slick-carousel/slick/slick-theme.css"; +import PropTypes from "prop-types"; +import StarRating from "../components/StarRating"; + +const DoctorSlider = ({ teamMembers = [] }) => { + const settings = { + dots: true, + infinite: true, + autoplay: true, + speed: 500, + slidesToShow: 4, + slidesToScroll: 2, + responsive: [ + { + breakpoint: 1024, + settings: { + slidesToShow: 3, + }, + }, + { + breakpoint: 768, + settings: { + slidesToShow: 2, + }, + }, + { + breakpoint: 640, + settings: { + slidesToShow: 1, + slidesToScroll: 1, + }, + }, + ], + }; + + return ( + + {teamMembers.map((member, index) => ( +
+
+
+ {member.name} +
+ +

{member.name}

+

{member.role}

+ +
+
+ ))} +
+ ); +}; + +DoctorSlider.propTypes = { + teamMembers: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + role: PropTypes.string.isRequired, + imageUrl: PropTypes.string.isRequired, + rating: PropTypes.number.isRequired, + }) + ), +}; + +export default DoctorSlider; diff --git a/src/main.jsx b/src/main.jsx index d13e088..2c5d34f 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -2,6 +2,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.jsx"; import "./index.css"; +import "./styles/slick.css"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; const router = createBrowserRouter([ diff --git a/src/sections/TopDoctorsSection.jsx b/src/sections/TopDoctorsSection.jsx new file mode 100644 index 0000000..df4ec64 --- /dev/null +++ b/src/sections/TopDoctorsSection.jsx @@ -0,0 +1,52 @@ +import Paragraph from "../components/Paragraph"; +import DoctorSlider from "../containers/DoctorSlider"; +import HeaderText from "../components/HeaderText"; +import image1 from "../assets/temp/img_01.webp"; +import image2 from "../assets/temp/img_02.webp"; +import image3 from "../assets/temp/img_03.webp"; +import image4 from "../assets/temp/img_04.webp"; +import image5 from "../assets/temp/img_05.webp"; + +const TopDoctorsSection = () => { + const teamMembers = [ + { + name: "Jubayer Al Hasan", + role: "Marketing Expert", + imageUrl: image1, + rating: 4, + }, + { + name: "Jannatul Ferdaus", + role: "Broker", + imageUrl: image2, + rating: 5, + }, + { + name: "Chris Matial", + role: "Broker", + imageUrl: image3, + rating: 3, + }, + { + name: "Mark Filo", + role: "CEO & Founder", + imageUrl: image4, + rating: 5, + }, + { + name: "Professor Dr D. Filo", + role: "CEO & Founder", + imageUrl: image5, + rating: 4, + }, + ]; + return ( + <> + + Lorem is placeholder text commonly used graphic + + + ); +}; + +export default TopDoctorsSection; diff --git a/src/stories/Container.stories.jsx b/src/stories/Container.stories.jsx new file mode 100644 index 0000000..bd40b46 --- /dev/null +++ b/src/stories/Container.stories.jsx @@ -0,0 +1,46 @@ +import Container from "../containers/Container"; + +export default { + title: "Components/Container", + component: Container, + tags: ["autodocs"], + argTypes: { + className: { control: "text" }, + children: { control: false }, + }, +}; + +// Default Container +export const Default = (args) => Default Content; + +Default.args = { + className: "", +}; + +// Container with Custom Padding +export const CustomPadding = (args) => Content with Custom Padding; + +CustomPadding.args = { + className: "p-8", +}; + +// Container with Background Color +export const WithBackgroundColor = (args) => Content with Background; + +WithBackgroundColor.args = { + className: "bg-gray-200", +}; + +// Container with Centered Text +export const CenteredText = (args) => Centered Text Content; + +CenteredText.args = { + className: "text-center", +}; + +// Container with Margin Adjustment +export const WithMarginAdjustment = (args) => Content with Margin Adjustment; + +WithMarginAdjustment.args = { + className: "my-10", +}; diff --git a/src/stories/DoctorSlider.stories.jsx b/src/stories/DoctorSlider.stories.jsx new file mode 100644 index 0000000..00b9f4f --- /dev/null +++ b/src/stories/DoctorSlider.stories.jsx @@ -0,0 +1,86 @@ +import "./assets/styles/slick.css"; +import image1 from "../assets/temp/img_01.webp"; +import image2 from "../assets/temp/img_02.webp"; +import image3 from "../assets/temp/img_03.webp"; +import image4 from "../assets/temp/img_04.webp"; +import image5 from "../assets/temp/img_05.webp"; +import DoctorSlider from "../containers/DoctorSlider"; + +export default { + title: "Components/DoctorSlider", + component: DoctorSlider, + tags: ["autodocs"], + argTypes: { + member1Name: { control: "text", name: "Member 1 Name" }, + member1Role: { control: "text", name: "Member 1 Role" }, + member1Rating: { control: "number", name: "Member 1 Rating" }, + member2Name: { control: "text", name: "Member 2 Name" }, + member2Role: { control: "text", name: "Member 2 Role" }, + member2Rating: { control: "number", name: "Member 2 Rating" }, + member3Name: { control: "text", name: "Member 3 Name" }, + member3Role: { control: "text", name: "Member 3 Role" }, + member3Rating: { control: "number", name: "Member 3 Rating" }, + member4Name: { control: "text", name: "Member 4 Name" }, + member4Role: { control: "text", name: "Member 4 Role" }, + member4Rating: { control: "number", name: "Member 4 Rating" }, + member5Name: { control: "text", name: "Member 5 Name" }, + member5Role: { control: "text", name: "Member 5 Role" }, + member5Rating: { control: "number", name: "Member 5 Rating" }, + }, +}; + +const Template = (args) => { + const teamMembers = [ + { + name: args.member1Name, + role: args.member1Role, + imageUrl: image1, + rating: args.member1Rating, + }, + { + name: args.member2Name, + role: args.member2Role, + imageUrl: image2, + rating: args.member2Rating, + }, + { + name: args.member3Name, + role: args.member3Role, + imageUrl: image3, + rating: args.member3Rating, + }, + { + name: args.member4Name, + role: args.member4Role, + imageUrl: image4, + rating: args.member4Rating, + }, + { + name: args.member5Name, + role: args.member5Role, + imageUrl: image5, + rating: args.member5Rating, + }, + ]; + + return ; +}; + +export const Default = Template.bind({}); +Default.args = { + member1Name: "Jubayer Al Hasan", + member1Role: "Marketing Expert", + member1Rating: 4, + member2Name: "Jannatul Ferdaus", + member2Role: "Broker", + member2Rating: 5, + member3Name: "Chris Matial", + member3Role: "Broker", + member3Rating: 3, + member4Name: "Mark Filo", + member4Role: "CEO & Founder", + member4Rating: 5, + member5Name: "Professor Dr D. Filo", + member5Role: "CEO & Founder", + member5Rating: 4, +}; diff --git a/src/stories/StarRating.stories.jsx b/src/stories/StarRating.stories.jsx new file mode 100644 index 0000000..bc88aab --- /dev/null +++ b/src/stories/StarRating.stories.jsx @@ -0,0 +1,41 @@ +import StarRating from "../components/StarRating"; +import "../styles/slick.css"; + +export default { + title: "Components/StarRating", + component: StarRating, + tags: ["autodocs"], + argTypes: { + rating: { + control: { + type: "number", + min: 0, + max: 5, + }, + }, + }, +}; + +// Default Star Rating +export const Default = (args) => ; +Default.args = { + rating: 3, // default rating +}; + +// 1-Star Rating +export const OneStar = (args) => ; +OneStar.args = { + rating: 1, +}; + +// 3-Star Rating +export const ThreeStars = (args) => ; +ThreeStars.args = { + rating: 3, +}; + +// 5-Star Rating +export const FiveStars = (args) => ; +FiveStars.args = { + rating: 5, +}; diff --git a/src/stories/assets/styles/slick.css b/src/stories/assets/styles/slick.css new file mode 100644 index 0000000..396fd0e --- /dev/null +++ b/src/stories/assets/styles/slick.css @@ -0,0 +1,21 @@ +.with-background::before { + content: ""; + position: absolute; + left: -40px; + right: -40px; + top: -35px; + height: 45%; + z-index: -1; + background: url("../../../assets/shape.svg") no-repeat 50%; + background-size: cover; +} +.slick-dots li.slick-active button:before { + opacity: 1 !important; + color: #f97316 !important; + font-size: 14px !important; +} +.slick-dots li button:before { + opacity: 1 !important; + color: #000 !important; + font-size: 14px !important; +} diff --git a/src/styles/slick.css b/src/styles/slick.css new file mode 100644 index 0000000..3a78598 --- /dev/null +++ b/src/styles/slick.css @@ -0,0 +1,43 @@ +.with-background::before { + content: ""; + position: absolute; + left: -40px; + right: -40px; + top: -35px; + height: 45%; + z-index: -1; + background: url("../assets/shape.svg") no-repeat 50%; + background-size: cover; +} +.slick-dots li.slick-active button:before { + @apply opacity-100 text-orange-600 text-[14px]; +} +.slick-dots li button:before { + @apply opacity-100 text-black text-[14px]; +} + +.doctor-section { + @apply relative; + + /* Left Line (Before) */ + &::before { + content: ""; + position: absolute; + width: calc(50% - 65px); + height: 2px; + background-color: #000; + bottom: 0; + left: 0; + } + + /* Right Line (After) */ + &::after { + content: ""; + position: absolute; + width: calc(50% - 65px); + height: 2px; + background-color: #000; + bottom: 0; + right: 0; + } +} diff --git a/tailwind.config.js b/tailwind.config.js index c7429c4..e1e1f22 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -2,11 +2,7 @@ const defaultColors = require("tailwindcss/colors"); /** @type {import('tailwindcss').Config} */ export default { - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - "./.storybook/**/*.{js,jsx,ts,tsx}", - ], + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}", "./.storybook/**/*.{js,jsx,ts,tsx}"], theme: { extend: { colors: { @@ -21,5 +17,15 @@ export default { }, }, }, - plugins: [], + plugins: [ + function ({ addUtilities }) { + addUtilities({ + ".slick-dot-active": { + opacity: "1 !important", + color: "#f97316 !important", + fontSize: "14px !important", + }, + }); + }, + ], };