├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .storybook
└── main.js
├── .stylelintrc.json
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── examples
├── home
│ ├── App.js
│ └── index.js
├── image-carousel
│ ├── App.js
│ ├── index.html
│ └── index.js
├── index.html
├── multi-rows-carousel
│ ├── App.js
│ ├── index.html
│ └── index.js
├── news-carousel
│ ├── App.js
│ ├── index.html
│ ├── index.js
│ └── mockNewsList.json
├── product-carousel
│ ├── App.js
│ ├── index.html
│ ├── index.js
│ └── mockProductList.json
├── responsive-carousel
│ ├── App.js
│ ├── index.html
│ └── index.js
├── simple-sample
│ ├── App.js
│ ├── index.html
│ └── index.js
├── testimonial-carousel
│ ├── App.js
│ ├── index.html
│ ├── index.js
│ └── mockTestimonialList.json
├── tour-carousel
│ ├── App.js
│ ├── cities.json
│ ├── index.html
│ └── index.js
└── webpack.config.js
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── app.js
├── components
│ ├── ArrowButton.js
│ ├── Carousel.js
│ └── Dot.js
├── hooks
│ └── responsiveLayoutHook.js
└── utils
│ └── resizeListener.js
└── stories
└── Carousel.stories.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"],
3 | "plugins": ["babel-plugin-styled-components"]
4 | }
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 | [*]
3 | end_of_line = lf
4 | insert_final_newline = true
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true
6 | },
7 | "extends": ["eslint:recommended", "plugin:react/recommended"],
8 | "globals": {
9 | "Atomics": "readonly",
10 | "SharedArrayBuffer": "readonly"
11 | },
12 | "parserOptions": {
13 | "ecmaFeatures": {
14 | "jsx": true
15 | },
16 | "ecmaVersion": 2018,
17 | "sourceType": "module"
18 | },
19 | "plugins": ["react", "react-hooks"],
20 | "rules": {
21 | "react/prop-types": 0,
22 | "react-hooks/rules-of-hooks": "error",
23 | "react-hooks/exhaustive-deps": "warn"
24 | },
25 | "settings": {
26 | "react": {
27 | "version": "detect"
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "endOfLine": "lf"
5 | }
6 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | stories: ['../stories/**/*.stories.js'],
3 | addons: [
4 | '@storybook/addon-actions',
5 | '@storybook/addon-links',
6 | '@storybook/addon-viewport/register',
7 | '@storybook/addon-knobs/register'
8 | ],
9 | webpackFinal: async config => {
10 | // do mutation to the config
11 |
12 | return config
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "processors": ["stylelint-processor-styled-components"],
3 | "extends": [
4 | "stylelint-config-standard",
5 | "stylelint-config-styled-components",
6 | "stylelint-config-prettier"
7 | ],
8 | "rules": {
9 | "declaration-colon-newline-after": null
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at z3388638@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 YY Chang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://nodei.co/npm/react-grid-carousel/)
2 | [](https://github.com/x3388638/react-grid-carousel/blob/master/LICENSE) [](https://www.npmjs.com/package/react-grid-carousel) [](https://opensource.org/)
3 |
4 |
React Grid Carousel
5 | React responsive carousel component w/ grid layout to easily create a carousel like photo gallery, shopping product card or anything you want
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ## Features
14 |
15 | - RWD
16 | - Multiple items
17 | - Multiple rows
18 | - Infinite loop
19 | - Support any component as a item to put into grid
20 | - Show/hide dots
21 | - Show/hide arrow buttons
22 | - Autoplay
23 | - Enable/Disable `scroll-snap` for each item on mobile device
24 | - Customized layout (cols & rows) for different breakpoint
25 | - Customized arrow button
26 | - Customized dots
27 | - Support SSR
28 |
29 | ## Install
30 |
31 | ```bash
32 | $ npm install react-grid-carousel --save
33 | ```
34 |
35 | ## Usage
36 |
37 | Just import the `Carousel` component from `react-grid-carousel`
38 | and put your item into `Carousel.Item`
39 |
40 | ```javascript
41 | import React from 'react'
42 | import Carousel from 'react-grid-carousel'
43 |
44 | const Gallery = () => {
45 | return (
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | {/* anything you want to show in the grid */}
58 |
59 | {/* ... */}
60 |
61 | )
62 | }
63 | ```
64 |
65 | ## Props
66 |
67 | | Prop | Type | Default | Description |
68 | | ------------------------------------- | ---------------- | --------- | ----------------------------------------------------------------------------------- |
69 | | cols | Number | 1 | Column amount rendered per page |
70 | | rows | Number | 1 | Row amount rendered per page |
71 | | gap | Number \| String | 10 | Margin (grid-gap) for each item/grid in px or %, passed Number will turn to px unit |
72 | | loop | Boolean | false | Infinite loop or not |
73 | | scrollSnap | Boolean | true | `true` for applying `scroll-snap` to items on mobile |
74 | | hideArrow | Boolean | false | Show/hide the arrow prev/next buttons |
75 | | showDots | Boolean | false | Show dots indicate the current page on desktop mode |
76 | | autoplay | Number | | Autoplay timeout in ms; `undefined` for autoplay disabled |
77 | | dotColorActive | String | '#795548' | Valid css color value for active dot |
78 | | dotColorInactive | String | '#ccc' | Valid css color value for inactive dot |
79 | | [responsiveLayout](#responsiveLayout) | Array | | Customized cols & rows on different viewport size |
80 | | mobileBreakpoint | Number | 767 | The breakpoint(px) to switch to default mobile layout |
81 | | arrowLeft | Element | | Customized left arrow button |
82 | | arrowRight | Element | | Customized left arrow button |
83 | | [dot](#dot) | Element | | Customized dot component with prop `isActive` |
84 | | containerStyle | Object | | Style object for carousel container |
85 |
86 | ### responsiveLayout
87 |
88 | Array of layout settings for each breakbpoint
89 |
90 | #### Setting options
91 |
92 | - `breakpoint`: Number; Requried; Equals to `max-width` used in media query, in px unit
93 | - `cols`: Number; Column amount in specific breakpoint
94 | - `rows`: Number; Row amount in specific breakpoint
95 | - `gap`: Number | String; Gap size in specific breakpoint
96 | - `loop`: Boolean; Infinite loop in specific breakpoint
97 | - `autoplay`: Number; autoplay timeout(ms) in specific breakpoint; `undefined` for autoplay disabled
98 |
99 | e.g.
100 |
101 | ```
102 | [
103 | {
104 | breakpoint: 800,
105 | cols: 3,
106 | rows: 1,
107 | gap: 10,
108 | loop: true,
109 | autoplay: 1000
110 | }
111 | ]
112 | ```
113 |
114 | ### dot
115 |
116 | #### Example
117 |
118 | ```javascript
119 | // your custom dot component with prop `isActive`
120 | const MyDot = ({ isActive }) => (
121 |
129 | )
130 |
131 | // set custom dot
132 |
133 | ```
134 |
135 | ## Example
136 |
137 | Storybook (Don't forget to try on different viewport size)
138 |
139 | ```bash
140 | $ git clone https://github.com/x3388638/react-grid-carousel
141 | $ cd react-grid-carousel
142 | $ npm ci
143 | $ npm run storybook
144 | ```
145 |
146 | Use case in real world
147 |
148 | ```bash
149 | # clone & install packages
150 | $ npm run dev
151 | # open localhost:8080
152 | ```
153 |
154 | or visit https://react-grid-carousel.now.sh/#use-case-in-real-world
155 |
156 | ## LICENSE
157 |
158 | MIT
159 |
--------------------------------------------------------------------------------
/examples/home/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from 'react'
2 | import styled from 'styled-components'
3 | import Carousel from '../../dist/bundle'
4 |
5 | const randomImgUrl = 'https://picsum.photos/{x}/{y}?random='
6 |
7 | const CustomBtn = styled.div`
8 | position: absolute;
9 | display: inline-flex;
10 | align-items: center;
11 | justify-content: center;
12 | height: 40px;
13 | width: 40px;
14 | font-size: 20px;
15 | color: red;
16 | opacity: 0.6;
17 | cursor: pointer;
18 | top: 50%;
19 | z-index: 10;
20 | transition: all 0.25s;
21 | transform: ${({ type }) =>
22 | `translateY(-50%) ${type === 'left' ? 'rotate(180deg)' : ''}`};
23 | left: ${({ type }) => (type === 'left' ? '20px' : 'initial')};
24 | right: ${({ type }) => (type === 'right' ? '20px' : 'initial')};
25 |
26 | &:hover {
27 | background: red;
28 | color: #fff;
29 | opacity: 0.5;
30 | }
31 | `
32 |
33 | const CustomDot = styled.span`
34 | display: inline-block;
35 | height: 8px;
36 | width: ${({ isActive }) => (isActive ? '16px' : '8px')};
37 | opacity: ${({ isActive }) => (isActive ? '0.8' : '0.5')};
38 | border-radius: 8px;
39 | background: red;
40 | transition: all 0.2s;
41 | `
42 |
43 | const App = () => {
44 | const [isHover, setIshover] = useState(false)
45 |
46 | const handleHover = useCallback(() => {
47 | setIshover(state => !state)
48 | }, [])
49 |
50 | return (
51 |
52 |
Single column
53 |
54 | {[...Array(4)].map((_, i) => (
55 |
56 |
60 |
61 | ))}
62 |
63 |
Multiple columns
64 |
65 | {[...Array(15)].map((_, i) => (
66 |
67 |
71 |
72 | ))}
73 |
74 |
Multiple cols + multiple rows
75 |
81 | {[...Array(18)].map((_, i) => (
82 |
83 |
87 |
88 | ))}
89 |
90 |
91 | Show/hide arrow buttons and dots w/ infinite loop
92 |
93 |
94 |
101 | {[...Array(9)].map((_, i) => (
102 |
103 |
109 |
110 | ))}
111 |
112 |
113 |
Autoplay w/ customized arrow buttons and dots
114 |
➜}
122 | arrowRight={➜ }
123 | dot={CustomDot}
124 | >
125 | {[...Array(20)].map((_, i) => (
126 |
127 |
131 |
132 | ))}
133 |
134 |
135 | Customized layout for RWD (max-width: 1000px/750px/500px)
136 |
137 |
138 | responsiveLayout settings
139 | {`[
140 | { breakpoint: 1000, cols: 3 },
141 | { breakpoint: 750, cols: 2, rows: 1, gap: 5 },
142 | { breakpoint: 499, autoplay: 2000, loop: true }
143 | ]`}
144 |
145 |
156 | {[...Array(20)].map((_, i) => (
157 |
158 |
162 |
163 | ))}
164 |
165 |
166 |
176 |
177 | )
178 | }
179 |
180 | export default App
181 |
--------------------------------------------------------------------------------
/examples/home/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | ReactDOM.render( , document.getElementById('root'))
6 |
--------------------------------------------------------------------------------
/examples/image-carousel/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from 'react'
2 | import styled from 'styled-components'
3 | import Carousel from '../../dist/bundle'
4 |
5 | const images = [
6 | 'https://a0.muscache.com/im/pictures/37e211bb-6ef8-44b6-8022-7427e7a241a5.jpg?aki_policy=large',
7 | 'https://a0.muscache.com/im/pictures/c571ab10-d896-4095-b4be-4e57aa8b43cd.jpg?aki_policy=large',
8 | 'https://a0.muscache.com/im/pictures/54b3eadc-e503-41da-a9fe-ba10d20d9aa6.jpg?aki_policy=large',
9 | 'https://a0.muscache.com/im/pictures/b2d713c1-1304-4363-a34f-19fc3c94bcd5.jpg?aki_policy=large',
10 | 'https://a0.muscache.com/im/pictures/6514fab6-a3c4-47b3-8f11-a736d6d3ff77.jpg?aki_policy=large'
11 | ]
12 |
13 | const Container = styled.div`
14 | position: absolute;
15 | top: 0;
16 | left: 0;
17 | min-height: 100%;
18 | width: 100%;
19 | `
20 |
21 | const HotelRow = styled.div`
22 | max-width: 800px;
23 | border-top: 1px solid #ebebeb;
24 | border-bottom: 1px solid #ebebeb;
25 | padding: 20px 0;
26 | margin: 30px auto;
27 | display: flex;
28 | position: relative;
29 |
30 | @media screen and (max-width: 767px) {
31 | padding: 20px 10px;
32 | flex-direction: column;
33 | }
34 | `
35 |
36 | const CarouselWapeer = styled.div`
37 | width: 340px;
38 |
39 | @media screen and (max-width: 767px) {
40 | margin-bottom: 20px;
41 | width: 100%;
42 | }
43 | `
44 |
45 | const Title = styled.div`
46 | font-size: 20px;
47 | `
48 |
49 | const Desc = styled.div`
50 | margin-top: 20px;
51 | font-size: 14px;
52 | color: gray;
53 |
54 | @media screen and (max-width: 767px) {
55 | margin-bottom: 50px;
56 | }
57 | `
58 | const Price = styled.div`
59 | position: absolute;
60 | bottom: 20px;
61 | right: 20px;
62 |
63 | span {
64 | font-weight: bold;
65 | font-size: 18px;
66 | }
67 | `
68 |
69 | const Code = styled.pre`
70 | max-width: 800px;
71 | margin: 0 auto;
72 | background: floralwhite;
73 | padding: 20px;
74 | box-sizing: border-box;
75 | overflow: auto;
76 | `
77 |
78 | const Reference = styled.div`
79 | margin: 50px auto;
80 | width: 100%;
81 | max-width: 800px;
82 | border-top: 1px solid #666;
83 |
84 | img {
85 | width: 100%;
86 | }
87 | `
88 |
89 | const App = () => {
90 | const [isHover, setIsHover] = useState(false)
91 |
92 | const handleHover = useCallback(() => {
93 | setIsHover(state => !state)
94 | }, [])
95 |
96 | return (
97 |
98 |
99 | Use{' '}
100 |
105 | react-grid-carousel
106 | {' '}
107 | to build image carousel
108 |
109 |
110 |
111 |
112 | {images.map((img, i) => (
113 |
114 |
115 |
116 | ))}
117 |
118 |
119 |
120 |
Sunny, Modern room in East Village!
121 |
122 | 1 guest · 1 bedroom · 1 bed · 1 shared bath
123 |
124 | Wifi · Kitchen · Heating · Air conditioning
125 |
126 |
127 | $1,952 TWD / night
128 |
129 |
130 |
131 | {`
132 | {images.map((img, i) => (
133 |
134 |
135 |
136 | ))}
137 | `}
138 |
139 |
140 | Image carousel on{' '}
141 |
146 | Airbnb
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 | )
155 | }
156 |
157 | export default App
158 |
--------------------------------------------------------------------------------
/examples/image-carousel/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Image carousel
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/examples/image-carousel/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | ReactDOM.render( , document.getElementById('root'))
6 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | React Grid Carousel
8 |
10 |
12 |
19 |
22 |
25 |
28 |
29 |
30 |
31 |
32 |
33 |
57 |
58 |
59 | $ npm i
60 | react-grid-carousel
61 | --save
62 |
63 |
64 | React resposive carousel component w/ grid layout to easily create a carousel like photo gallery, shopping product
65 | card or anything you want
66 |
67 |
GitHub
69 |
70 |
71 |
Example
72 |
73 |
Use case in real world
74 |
86 |
Features
87 |
88 | RWD
89 | Multiple items
90 | Multiple rows
91 | Infinite loop
92 | Support any component as a item to put into grid
93 | Show/hide dots
94 | Show/hide arrow buttons
95 | Auto play
96 | Enable/Disable `scroll-snap` for each item on mobile device
97 | Customized layout (cols & rows) for different breakpoint
98 | Customized arrow button
99 | Support SSR
100 |
101 |
Install
102 |
$ npm install react-grid-carousel --save
103 |
Usage
104 |
105 | import React from 'react'
106 | import Carousel from 'react-grid-carousel'
107 |
108 | const Gallery = () => {
109 | return (
110 | <Carousel cols={2} rows={1} gap={10} loop>
111 | <Carousel.Item>
112 | <img width="100%" src="https://picsum.photos/800/600?random=1" />
113 | </Carousel.Item>
114 | <Carousel.Item>
115 | <img width="100%" src="https://picsum.photos/800/600?random=2" />
116 | </Carousel.Item>
117 | <Carousel.Item>
118 | <img width="100%" src="https://picsum.photos/800/600?random=3" />
119 | </Carousel.Item>
120 | <Carousel.Item>
121 | {/* anything you want to show in the grid */}
122 | </Carousel.Item>
123 | {/* ... */}
124 | </Carousel>
125 | )
126 | }
127 |
Docs
128 |
https://github.com/x3388638/react-grid-carousel
130 |
133 |
134 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/examples/multi-rows-carousel/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import Carousel from '../../dist/bundle'
4 |
5 | const brandLogo = 'https://fakeimg.pl/320x180/?text=Brand%20logo%20'
6 |
7 | const Container = styled.div`
8 | position: absolute;
9 | top: 0;
10 | left: 0;
11 | width: 100%;
12 | min-height: 100%;
13 | background: #f5f5f5;
14 | `
15 |
16 | const Row = styled.div`
17 | max-width: 1200px;
18 | margin: 0 auto;
19 | `
20 |
21 | const Logo = styled.div`
22 | height: 110px;
23 | background-image: ${({ img }) => `url(${img})`};
24 | background-size: cover;
25 | background-position: center;
26 | background-repeat: no-repeat;
27 | cursor: pointer;
28 |
29 | &:hover {
30 | border: 1px solid #a9a9a9;
31 | width: calc(100% - 2px);
32 | height: 108px;
33 | }
34 | `
35 |
36 | const More = styled.div`
37 | height: 100%;
38 | width: 100%;
39 | display: flex;
40 | align-items: center;
41 | justify-content: center;
42 | color: red;
43 | font-weight: bold;
44 | cursor: pointer;
45 | background: #fff;
46 |
47 | &:hover {
48 | border: 1px solid #a9a9a9;
49 | width: calc(100% - 2px);
50 | height: 108px;
51 | }
52 | `
53 |
54 | const Code = styled.pre`
55 | max-width: 1200px;
56 | margin: 15px auto;
57 | background: #fff;
58 | padding: 20px;
59 | box-sizing: border-box;
60 | overflow: auto;
61 | `
62 |
63 | const Reference = styled.div`
64 | margin: 50px auto;
65 | width: 100%;
66 | max-width: 1200px;
67 | border-top: 1px solid #666;
68 |
69 | img {
70 | width: 100%;
71 | }
72 | `
73 |
74 | const App = () => {
75 | return (
76 |
77 |
78 | Use{' '}
79 |
84 | react-grid-carousel
85 | {' '}
86 | to build multiple rows carousel
87 |
88 |
89 |
95 | {[...Array(23)].map((_, i) => (
96 |
97 |
98 |
99 | ))}
100 |
101 | See All >
102 |
103 |
104 |
105 | {`
111 | {[...Array(23)].map((_, i) => (
112 |
113 |
114 |
115 | ))}
116 |
117 | See All >
118 |
119 | `}
120 |
121 |
122 | Multiple rows carousel on{' '}
123 |
124 | Shopee
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | )
133 | }
134 |
135 | export default App
136 |
--------------------------------------------------------------------------------
/examples/multi-rows-carousel/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Multiple rows carousel
8 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/examples/multi-rows-carousel/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | ReactDOM.render( , document.getElementById('root'))
6 |
--------------------------------------------------------------------------------
/examples/news-carousel/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import Carousel from '../../dist/bundle'
4 | import newsList from './mockNewsList.json'
5 |
6 | const Container = styled.div`
7 | position: absolute;
8 | top: 0;
9 | left: 0;
10 | min-height: 100%;
11 | width: 100%;
12 | max-width: 500px;
13 | background: #f3f3f3;
14 | `
15 |
16 | const Notice = styled.div`
17 | margin: 10px;
18 | color: lightcoral;
19 | `
20 |
21 | const Item = styled.div`
22 | position: relative;
23 | height: 205px;
24 | background-image: ${({ img }) => `url(${img})`};
25 | background-position: center;
26 | background-size: cover;
27 | background-repeat: no-repeat;
28 | border-radius: 8px;
29 | `
30 |
31 | const Index = styled.div`
32 | position: absolute;
33 | background: #232323bf;
34 | color: #ffffffc9;
35 | padding: 2px 8px;
36 | font-size: 12px;
37 | border-radius: 20px;
38 | top: 5px;
39 | right: 5px;
40 | `
41 |
42 | const Detail = styled.div`
43 | position: absolute;
44 | bottom: 0;
45 | color: #fff;
46 | padding: 15px;
47 | width: 100%;
48 | box-sizing: border-box;
49 | background: linear-gradient(0deg, black, transparent);
50 | padding-top: 120px;
51 | border-radius: 8px;
52 | `
53 |
54 | const Title = styled.div`
55 | font-size: 20px;
56 | font-weight: bold;
57 | `
58 | const Comment = styled.div`
59 | font-size: 16px;
60 | font-weight: bold;
61 | `
62 |
63 | const Code = styled.pre`
64 | background: #fff;
65 | padding: 20px;
66 | font-size: 12px;
67 | overflow: auto;
68 | `
69 |
70 | const Reference = styled.div`
71 | margin: 50px auto;
72 | border-top: 1px solid #666;
73 | `
74 |
75 | const App = () => {
76 | return (
77 |
78 |
79 | Use{' '}
80 |
85 | react-grid-carousel
86 | {' '}
87 | to build news carousel
88 |
89 | Notice: You should try this demo on mobile viewport size
90 |
91 | {newsList.map(({ imageSrc, title, comment }, i) => (
92 |
93 | -
94 |
95 | {i + 1}/{newsList.length}
96 |
97 |
98 | {title}
99 | {!!comment && {comment.count} comments }
100 |
101 |
102 |
103 | ))}
104 |
105 | {`
106 | {newsList.map(({ imageSrc, title, comment }, i) => (
107 |
108 | -
109 |
110 | {i + 1}/{newsList.length}
111 |
112 |
113 | {title}
114 | {!!comment && {comment.count} comments }
115 |
116 |
117 |
118 | ))}
119 | `}
120 |
121 |
131 |
134 |
143 |
144 |
145 |
146 | )
147 | }
148 |
149 | export default App
150 |
--------------------------------------------------------------------------------
/examples/news-carousel/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | News carousel
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/examples/news-carousel/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | ReactDOM.render( , document.getElementById('root'))
6 |
--------------------------------------------------------------------------------
/examples/news-carousel/mockNewsList.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "comment": {
4 | "detail": "兩岸全面斷航⋯⋯",
5 | "nickname": "reen ho",
6 | "profile": "https://s.yimg.com/gq/1770/40147157126_80fc42_o.jpg",
7 | "count": 32
8 | },
9 | "imageSrc": "https://s1.yimg.com/uu/api/res/1.2/k54NZ.sirRkPkEgUpmOM1w--/YXBwaWQ9eXRhY2h5b247Y2g9NDQ5O2N3PTgwMDtkeD0wO2R5PTA7Zmk9dWxjcm9wO2g9MzAwO3c9ODMwOw--/https://media.zenfs.com/en/cna.com.tw/d031a8727dc4d6f296c3a4fe8ff523d9",
10 | "imageWebpSrc": "https://s2.yimg.com/lo/api/res/1.2/Tcq8S8O2BfJ_REWDxUq8Ag--/YXBwaWQ9eXR3ZnBhZ2U-/https://s1.yimg.com/uu/api/res/1.2/k54NZ.sirRkPkEgUpmOM1w--/YXBwaWQ9eXRhY2h5b247Y2g9NDQ5O2N3PTgwMDtkeD0wO2R5PTA7Zmk9dWxjcm9wO2g9MzAwO3c9ODMwOw--/https://media.zenfs.com/en/cna.com.tw/d031a8727dc4d6f296c3a4fe8ff523d9.cf.webp",
11 | "link": "https://tw.news.yahoo.com/%E6%AD%A6%E6%BC%A2%E8%82%BA%E7%82%8E%E5%8F%B0%E7%81%A3%E6%96%B0%E5%A2%9E2%E4%BE%8B-%E6%94%BF%E5%BA%9C%E6%B1%BA%E5%AE%9A3%E6%8E%AA%E6%96%BD-114243142.html",
12 | "followLink": false,
13 | "title": "新增2例入台 政府要求湖北團快離境",
14 | "type": "story",
15 | "uuid": "f68fb997-aa08-3716-9c21-0c575fdb52d2"
16 | },
17 | {
18 | "comment": {
19 | "detail": "人類夕鶴,中國幫了大忙",
20 | "nickname": "無忌",
21 | "profile": "https://s.yimg.com/gq/1756/40147164326_afa8df_o.jpg",
22 | "count": 8
23 | },
24 | "imageSrc": "https://s.yimg.com/uu/api/res/1.2/CpM3siULXbwr6mbJulW82Q--/YXBwaWQ9eXRhY2h5b247Y2g9MzU4O2N3PTY0MDtkeD0wO2R5PTA7Zmk9dWxjcm9wO2g9MzAwO3c9ODMwOw--/http://media.zenfs.com/it-IT/video/funweek_25/8491ae2f81ed8ae5c22457ea624e617d",
25 | "imageWebpSrc": "https://s.yimg.com/lo/api/res/1.2/4U6HVA8Nl_GNkVlhuFtOOQ--/YXBwaWQ9eXR3ZnBhZ2U-/https://s.yimg.com/uu/api/res/1.2/CpM3siULXbwr6mbJulW82Q--/YXBwaWQ9eXRhY2h5b247Y2g9MzU4O2N3PTY0MDtkeD0wO2R5PTA7Zmk9dWxjcm9wO2g9MzAwO3c9ODMwOw--/http://media.zenfs.com/it-IT/video/funweek_25/8491ae2f81ed8ae5c22457ea624e617d.cf.webp",
26 | "link": "https://tw.news.yahoo.com/%E6%9C%AB%E6%97%A5%E9%90%98%E8%B7%9D%E5%8D%88%E5%A4%9C%E5%8F%AA%E5%89%A9100%E7%A7%92-73%E5%B9%B4%E4%BE%86%E4%BA%BA%E9%A1%9E%E6%9C%80%E6%8E%A5%E8%BF%91%E6%BB%85%E4%BA%A1-064131854.html",
27 | "followLink": false,
28 | "title": "只剩100秒 73年來人類最接近滅亡",
29 | "type": "story",
30 | "uuid": "799ea104-b8bd-333b-bf4e-7eb9e04f9986"
31 | },
32 | {
33 | "comment": {
34 | "detail": "人們賦予錢太大的力量了,以致於人性在錢的面前醜態百出⋯⋯",
35 | "nickname": "無名",
36 | "profile": "https://s.yimg.com/gq/1756/40147164326_afa8df_o.jpg",
37 | "count": 7
38 | },
39 | "imageSrc": "https://s3.yimg.com/uu/api/res/1.2/u14p5sDcBUvG1RgqL5nOtw--/YXBwaWQ9eXRhY2h5b247Y2g9ODgwO2N3PTEyMDA7ZHg9MDtkeT0wO2ZpPXVsY3JvcDtoPTMwMDt3PTgzMDs-/http://media.zenfs.com/zh-Hant-TW/homerun/bcc.com.tw/89bfa53e5be1c18dd1d164ec024a652a",
40 | "imageWebpSrc": "https://s.yimg.com/lo/api/res/1.2/apFS8ELEkmRyrrjBqcdzcg--/YXBwaWQ9eXR3ZnBhZ2U-/https://s3.yimg.com/uu/api/res/1.2/u14p5sDcBUvG1RgqL5nOtw--/YXBwaWQ9eXRhY2h5b247Y2g9ODgwO2N3PTEyMDA7ZHg9MDtkeT0wO2ZpPXVsY3JvcDtoPTMwMDt3PTgzMDs-/http://media.zenfs.com/zh-Hant-TW/homerun/bcc.com.tw/89bfa53e5be1c18dd1d164ec024a652a.cf.webp",
41 | "link": "https://tw.news.yahoo.com/video/%E7%B4%AB%E5%8D%97%E5%AE%AE%E5%88%9D-%E7%99%BC%E9%8C%A2%E6%AF%8D-%E6%8E%92%E9%9A%8A%E9%A0%AD%E9%A6%994%E5%A4%A9%E5%89%8D%E5%88%B0-%E5%BB%9F%E6%96%B9%E5%82%9910%E8%90%AC%E6%9E%9A-060235929.html",
42 | "followLink": false,
43 | "title": "大動員!搶紫南宮錢母長龍無限延伸",
44 | "type": "video",
45 | "uuid": "a0729e59-5c45-3ed5-b011-8466e1dea037"
46 | },
47 | {
48 | "comment": {
49 | "detail": "武漢人過這個年很淦吧!物價飆升七倍還被封城,這不叫過年,這叫坐牢。",
50 | "nickname": "jack",
51 | "profile": "https://s.yimg.com/ag/images/1735/55563583803_ec658c_192sq.jpg",
52 | "count": 20
53 | },
54 | "imageSrc": "https://s1.yimg.com/uu/api/res/1.2/jhy5BICj.98lFA4a3TuRHg--/YXBwaWQ9eXRhY2h5b247Y2g9MjgxMjtjdz01MDAwO2R4PTA7ZHk9MzcyO2ZpPXVsY3JvcDtoPTMwMDt3PTgzMDs-/https://media-mbst-pub-ue1.s3.amazonaws.com/creatr-images/2020-01/e05f7e90-3e72-11ea-99df-fe4bb0892449",
55 | "imageWebpSrc": "https://s1.yimg.com/lo/api/res/1.2/lu9GYaPGoifPPmGDJxUfog--/YXBwaWQ9eXR3ZnBhZ2U-/https://s1.yimg.com/uu/api/res/1.2/jhy5BICj.98lFA4a3TuRHg--/YXBwaWQ9eXRhY2h5b247Y2g9MjgxMjtjdz01MDAwO2R4PTA7ZHk9MzcyO2ZpPXVsY3JvcDtoPTMwMDt3PTgzMDs-/https://media-mbst-pub-ue1.s3.amazonaws.com/creatr-images/2020-01/e05f7e90-3e72-11ea-99df-fe4bb0892449.cf.webp",
56 | "link": "https://tw.news.yahoo.com/%E6%9C%AC%E6%97%A5-yahoo%E7%84%A6%E9%BB%9E%E9%99%B8%E5%AE%A2%E6%96%B7%E7%82%8A%E5%BE%8C-%E5%A2%BE%E4%B8%81%E5%81%B7%E7%AC%91-%E5%8D%97%E6%8A%95%E5%BF%83%E5%AF%92-073457271.html",
57 | "followLink": false,
58 | "title": "台確診病例納中國⋯我促世衛更正",
59 | "type": "story",
60 | "uuid": "81b59f1a-a9a3-3174-994b-a3f88b3e4a42"
61 | },
62 | {
63 | "comment": null,
64 | "imageSrc": "https://s.yimg.com/uu/api/res/1.2/PAqwZH6vOKrP2FQmLjXruw--/YXBwaWQ9eXRhY2h5b247Y2g9MjM5O2N3PTQyNDtkeD0wO2R5PTA7Zmk9dWxjcm9wO2g9MzAwO3c9ODMwOw--/https://s.yimg.com/os/creatr-images/GLB/2017-08-14/93c7edd0-80c7-11e7-9413-0740cae646c9_iStock_000003018523XSmall.jpg",
65 | "imageWebpSrc": "https://s1.yimg.com/lo/api/res/1.2/xQ9vkz9eLCXMqc83A3PpVw--/YXBwaWQ9eXR3ZnBhZ2U-/https://s.yimg.com/uu/api/res/1.2/PAqwZH6vOKrP2FQmLjXruw--/YXBwaWQ9eXRhY2h5b247Y2g9MjM5O2N3PTQyNDtkeD0wO2R5PTA7Zmk9dWxjcm9wO2g9MzAwO3c9ODMwOw--/https://s.yimg.com/os/creatr-images/GLB/2017-08-14/93c7edd0-80c7-11e7-9413-0740cae646c9_iStock_000003018523XSmall.jpg.cf.webp",
66 | "link": "https://tw.travel.yahoo.com/news/%E9%81%8E%E5%B9%B4%E5%A1%9E%E8%BB%8A%E4%B8%8D%E7%84%A1%E8%81%8A%E8%B6%85%E5%AF%A6%E7%94%A8%E8%BB%8A%E4%B8%8A%E7%8E%A9%E6%A8%82%E6%B3%95%E5%AF%B6%E9%99%AA%E4%BD%A0%E6%AE%BA%E6%99%82%E9%96%93-031130985.html",
67 | "followLink": false,
68 | "title": "塞車必備救星 少了它全都沒得玩",
69 | "type": "story",
70 | "uuid": "94ae1c54-3c80-30f3-8247-89a3c7c37ad7"
71 | },
72 | {
73 | "comment": {
74 | "detail": "我回想起,SARS時,大陸官員在WHO會議上對台灣採"誰理你們"的態度。",
75 | "nickname": "K",
76 | "profile": "https://s.yimg.com/gq/1777/40147150166_5af2d8_o.jpg",
77 | "count": 52
78 | },
79 | "imageSrc": "https://s1.yimg.com/uu/api/res/1.2/DRvrU3w.g3UsWkfVULSBrQ--/YXBwaWQ9eXRhY2h5b247Y2g9MTQwMDtjdz0yMDgxO2R4PTA7ZHk9MDtmaT11bGNyb3A7aD0zMDA7dz04MzA7/https://media-mbst-pub-ue1.s3.amazonaws.com/creatr-uploaded-images/2020-01/a94aee40-3e99-11ea-adff-6fe3b6204f7a",
80 | "imageWebpSrc": "https://s1.yimg.com/lo/api/res/1.2/EZakeXqzzKJKwy.FdQv6ww--/YXBwaWQ9eXR3ZnBhZ2U-/https://s1.yimg.com/uu/api/res/1.2/DRvrU3w.g3UsWkfVULSBrQ--/YXBwaWQ9eXRhY2h5b247Y2g9MTQwMDtjdz0yMDgxO2R4PTA7ZHk9MDtmaT11bGNyb3A7aD0zMDA7dz04MzA7/https://media-mbst-pub-ue1.s3.amazonaws.com/creatr-uploaded-images/2020-01/a94aee40-3e99-11ea-adff-6fe3b6204f7a.cf.webp",
81 | "link": "https://tw.news.yahoo.com/video/%E9%86%AB%E9%99%A2%E5%A1%9E%E7%88%86-%E6%97%A5%E8%BC%AA4%E7%8F%AD-%E6%AD%A6%E6%BC%A2%E9%86%AB%E7%94%9F-%E7%B4%AF%E5%B4%A9-%E9%A3%86%E9%AB%98%E5%B1%A4-055108107.html",
82 | "followLink": false,
83 | "title": "武漢醫護加班崩潰 通話怒飆高層",
84 | "type": "video",
85 | "uuid": "ff98be35-85bb-3057-8463-aba771bb7c80"
86 | },
87 | {
88 | "comment": {
89 | "detail": "大家都乖乖待在家裡頭打電動就好 打麻將還要四個人",
90 | "nickname": "一切有為法,如夢幻泡影,如露亦如電,應作如是觀",
91 | "profile": "https://s.yimg.com/gq/1756/40147164326_afa8df_o.jpg",
92 | "count": 21
93 | },
94 | "imageSrc": "https://s3.yimg.com/uu/api/res/1.2/ylz.rsqLKBn0BN0dbI0Gzg--/YXBwaWQ9eXRhY2h5b247Y2g9NDUyO2N3PTgwNDtkeD0yMjtkeT0wO2ZpPXVsY3JvcDtoPTMwMDt3PTgzMDs-/https://media.zenfs.com/zh-TW/ctwant_com_582/fd1d3c82f58e6b29fa3d006bb6585ca4",
95 | "imageWebpSrc": "https://s.yimg.com/lo/api/res/1.2/KSw.ejIBjY6I0HUiUhMGtw--/YXBwaWQ9eXR3ZnBhZ2U-/https://s3.yimg.com/uu/api/res/1.2/ylz.rsqLKBn0BN0dbI0Gzg--/YXBwaWQ9eXRhY2h5b247Y2g9NDUyO2N3PTgwNDtkeD0yMjtkeT0wO2ZpPXVsY3JvcDtoPTMwMDt3PTgzMDs-/https://media.zenfs.com/zh-TW/ctwant_com_582/fd1d3c82f58e6b29fa3d006bb6585ca4.cf.webp",
96 | "link": "https://tw.news.yahoo.com/sars%E9%83%BD%E6%B2%92%E9%96%89%E9%A4%A8-%E5%8C%97%E4%BA%AC%E6%95%85%E5%AE%AE%E9%A6%96%E6%AC%A1%E9%96%89%E9%A4%A8-050100955.html",
97 | "followLink": false,
98 | "title": "抗SARS都沒這樣 北京故宮罕見閉館",
99 | "type": "story",
100 | "uuid": "a30fdf39-24b5-3e2f-a521-359798018641"
101 | },
102 | {
103 | "comment": {
104 | "detail": "今年的年好冷喔!(冷清)為啥呢?",
105 | "nickname": "Test",
106 | "profile": "https://s.yimg.com/gq/1746/29332403839_af81b0_o.jpg",
107 | "count": 21
108 | },
109 | "imageSrc": "https://s.yimg.com/uu/api/res/1.2/4Hy95YzMHxlNtRVtAGe.cw--/YXBwaWQ9eXRhY2h5b247Y2g9MzI5O2N3PTYwMDtkeD0wO2R5PTA7Zmk9dWxjcm9wO2g9MzAwO3c9ODMwOw--/https://media-mbst-pub-ue1.s3.amazonaws.com/creatr-uploaded-images/2020-01/b3509480-3e80-11ea-bdb5-0696ab814b35",
110 | "imageWebpSrc": "https://s.yimg.com/lo/api/res/1.2/P8N22WJCoE8W49rXPBJQEg--/YXBwaWQ9eXR3ZnBhZ2U-/https://s.yimg.com/uu/api/res/1.2/4Hy95YzMHxlNtRVtAGe.cw--/YXBwaWQ9eXRhY2h5b247Y2g9MzI5O2N3PTYwMDtkeD0wO2R5PTA7Zmk9dWxjcm9wO2g9MzAwO3c9ODMwOw--/https://media-mbst-pub-ue1.s3.amazonaws.com/creatr-uploaded-images/2020-01/b3509480-3e80-11ea-bdb5-0696ab814b35.cf.webp",
111 | "link": "https://tw.news.yahoo.com/video/%E5%BD%B0%E5%8C%9629%E5%B9%B4%E8%9B%8B%E9%A4%85%E8%80%81%E5%BA%97%E5%B0%87%E6%AD%87%E6%A5%AD-%E6%B0%91%E7%9C%BE%E5%96%8A-%E5%8F%AF%E4%BB%A5%E4%B8%8D%E8%A6%81%E9%97%9C%E5%97%8E-063411210.html",
112 | "followLink": false,
113 | "title": "「可以別關嗎」29年古早味老店收了",
114 | "type": "video",
115 | "uuid": "e4850219-e355-32e9-8909-daec8fc546c9"
116 | },
117 | {
118 | "comment": {
119 | "detail": "官方慣性隱匿疫情、粉飾太平 , 疫情早就擴散 , 現在封城已經枉然了 ! \n尤其是 , 還拉著全世界一起下水 !",
120 | "nickname": "oldman",
121 | "profile": "https://s.yimg.com/gq/1721/38837678613_ebe081_o.jpg",
122 | "count": 219
123 | },
124 | "imageSrc": "https://s3.yimg.com/uu/api/res/1.2/k5xq5OKkcbjrOo9Eez0WSQ--/YXBwaWQ9eXRhY2h5b247Y2g9MjQ5O2N3PTQ0MjtkeD0wO2R5PTg0O2ZpPXVsY3JvcDtoPTMwMDt3PTgzMDs-/https://media.zenfs.com/ko/setn.com.tw/393c38a4b03d142a8db782af4b3814e5",
125 | "imageWebpSrc": "https://s1.yimg.com/lo/api/res/1.2/cts5cokLnFfbiZSczuurhA--/YXBwaWQ9eXR3ZnBhZ2U-/https://s3.yimg.com/uu/api/res/1.2/k5xq5OKkcbjrOo9Eez0WSQ--/YXBwaWQ9eXRhY2h5b247Y2g9MjQ5O2N3PTQ0MjtkeD0wO2R5PTg0O2ZpPXVsY3JvcDtoPTMwMDt3PTgzMDs-/https://media.zenfs.com/ko/setn.com.tw/393c38a4b03d142a8db782af4b3814e5.cf.webp",
126 | "link": "https://tw.news.yahoo.com/%E6%AD%A6%E6%BC%A2%E5%B0%81%E5%9F%8E%E5%BE%8C%E7%9A%84%E5%B8%82%E6%B0%91%E7%94%9F%E6%B4%BB-%E8%B6%85%E5%B8%82-%E6%8E%83%E8%80%8C%E7%A9%BA-%E9%86%AB%E9%99%A2%E4%BA%BA%E6%BB%BF%E7%82%BA%E6%82%A3-%E5%83%8F%E6%98%AF-023001964.html",
127 | "followLink": false,
128 | "title": "武漢封城後⋯「像惡靈古堡一樣」",
129 | "type": "story",
130 | "uuid": "1a964599-c1ae-3eef-a851-8b73a4d31dca"
131 | },
132 | {
133 | "comment": {
134 | "detail": "有病",
135 | "nickname": "無忌",
136 | "profile": "https://s.yimg.com/gq/1756/40147164326_afa8df_o.jpg",
137 | "count": 2
138 | },
139 | "imageSrc": "https://s3.yimg.com/uu/api/res/1.2/BaaVFBVgnixXG_48t6aWBQ--/YXBwaWQ9eXRhY2h5b247Y2g9NjAyO2N3PTkxMjtkeD0wO2R5PTA7Zmk9dWxjcm9wO2g9MzAwO3c9ODMwOw--/https://media-mbst-pub-ue1.s3.amazonaws.com/creatr-uploaded-images/2020-01/34d9fa80-3e83-11ea-b6ff-6c02b46a4428",
140 | "imageWebpSrc": "https://s2.yimg.com/lo/api/res/1.2/X7S9WVSnxiliowbDq7hTOw--/YXBwaWQ9eXR3ZnBhZ2U-/https://s3.yimg.com/uu/api/res/1.2/BaaVFBVgnixXG_48t6aWBQ--/YXBwaWQ9eXRhY2h5b247Y2g9NjAyO2N3PTkxMjtkeD0wO2R5PTA7Zmk9dWxjcm9wO2g9MzAwO3c9ODMwOw--/https://media-mbst-pub-ue1.s3.amazonaws.com/creatr-uploaded-images/2020-01/34d9fa80-3e83-11ea-b6ff-6c02b46a4428.cf.webp",
141 | "link": "https://tw.tv.yahoo.com/%E4%B8%89%E7%94%9F%E4%B8%89%E4%B8%96%E6%9E%95%E4%B8%8A%E6%9B%B8-%E7%8D%A8%E5%AE%B6%E9%A0%90%E5%91%8A-081051301.html",
142 | "followLink": false,
143 | "title": "愛而不得三情斷 虐戀只求被記得",
144 | "type": "video",
145 | "uuid": "12628b04-cb30-3921-a68f-c839705466fa"
146 | }
147 | ]
148 |
--------------------------------------------------------------------------------
/examples/product-carousel/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import Carousel from '../../dist/bundle'
4 | import products from './mockProductList.json'
5 |
6 | const Body = styled.div`
7 | background: #f3f3f3;
8 | position: absolute;
9 | top: 0;
10 | left: 0;
11 | min-height: 100%;
12 | width: 100%;
13 | `
14 |
15 | const CarouselContainer = styled.div`
16 | padding: 20px 0;
17 | `
18 |
19 | const Row = styled.div`
20 | max-width: 1000px;
21 | margin: 10px auto;
22 | border-radius: 8px;
23 | background: #fff;
24 |
25 | @media screen and (max-width: 767px) {
26 | margin: 10px;
27 | }
28 | `
29 |
30 | const RowHead = styled.div`
31 | padding: 20px;
32 | font-size: 18px;
33 | font-weight: bold;
34 | border-bottom: 1px solid #eee;
35 | `
36 |
37 | const Card = styled.div`
38 | position: relative;
39 |
40 | img {
41 | width: 100%;
42 | height: 180px;
43 | object-fit: cover;
44 | border-radius: 8px;
45 | }
46 |
47 | span:first-of-type {
48 | color: red;
49 | font-size: 16px;
50 | font-weight: bold;
51 | }
52 |
53 | span:last-of-type {
54 | color: gray;
55 | font-size: 12px;
56 | text-decoration-line: line-through;
57 | margin-left: 10px;
58 | }
59 |
60 | @media screen and (max-width: 767px) {
61 | background: #f3f3f3;
62 | border: 1px solid #f3f3f3;
63 | }
64 | `
65 |
66 | const Title = styled.div`
67 | font-size: 14px;
68 | line-height: 16px;
69 | height: 32px;
70 | overflow: hidden;
71 | margin-bottom: 5px;
72 | `
73 |
74 | const Mask = styled.div`
75 | opacity: 0;
76 | height: 100%;
77 | width: 100%;
78 | cursor: pointer;
79 | background: #0000000a;
80 | position: absolute;
81 | border-radius: 8px;
82 | top: 0;
83 | left: 0;
84 |
85 | &:hover {
86 | opacity: 1;
87 | }
88 | `
89 |
90 | const Code = styled.pre`
91 | max-width: 1000px;
92 | margin: 10px auto;
93 | background: #fff;
94 | padding: 20px;
95 | box-sizing: border-box;
96 | `
97 |
98 | const Reference = styled.div`
99 | margin: 50px auto;
100 | width: 100%;
101 | max-width: 1000px;
102 | border-top: 1px solid #666;
103 |
104 | img {
105 | width: 100%;
106 | }
107 | `
108 |
109 | const App = () => {
110 | return (
111 |
112 |
113 | Use{' '}
114 |
119 | react-grid-carousel
120 | {' '}
121 | to build product carousel
122 |
123 |
124 | 每日好康
125 |
126 |
127 | {products.map((val, i) => (
128 |
129 |
130 |
131 |
132 |
{val.title}
133 | {val.specialPrice}
134 | {val.oriPrice}
135 |
136 |
137 |
138 |
139 | ))}
140 |
141 |
142 |
143 |
144 | {`
145 | {products.map((val, i) => (
146 |
147 |
148 |
149 |
150 |
{val.title}
151 | {val.specialPrice}
152 | {val.oriPrice}
153 |
154 |
155 |
156 |
157 | ))}
158 | `}
159 |
160 |
161 |
171 |
172 |
177 |
178 |
179 |
180 |
181 | )
182 | }
183 |
184 | export default App
185 |
--------------------------------------------------------------------------------
/examples/product-carousel/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Product carousel
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/examples/product-carousel/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | ReactDOM.render( , document.getElementById('root'))
6 |
--------------------------------------------------------------------------------
/examples/product-carousel/mockProductList.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "img": "https://s.yimg.com/zp/MerchandiseImages/38D5C91EBF-Product-20678834.jpg",
4 | "title": "美國iRobot Roomba960智慧吸塵+wifi掃地機器人(總代理保固1+1年)",
5 | "specialPrice": "$17999",
6 | "oriPrice": "$34999"
7 | },
8 | {
9 | "img": "https://s.yimg.com/zp/MerchandiseImages/339BEEDBF1-Gd-8615522.jpg",
10 | "title": "過年換新機 iPhone 11 新機熱賣搶購中",
11 | "specialPrice": "$23688",
12 | "oriPrice": "$24900"
13 | },
14 | {
15 | "img": "https://s.yimg.com/zp/MerchandiseImages/AD34BB3446-Gd-7448332.jpg",
16 | "title": "天天喝好水 礦泉水箱購送到家35折起",
17 | "specialPrice": "$115",
18 | "oriPrice": "$200"
19 | },
20 | {
21 | "img": "https://s.yimg.com/zp/MerchandiseImages/39216872AC-Gd-8778203.jpg",
22 | "title": "濕巾x衛生棉 滿599折100",
23 | "specialPrice": "折100",
24 | "oriPrice": "$1199"
25 | },
26 | {
27 | "img": "https://s.yimg.com/zp/MerchandiseImages/789778D0E9-Gd-8129802.jpg",
28 | "title": "(24H到貨) LONGCHAMP 經典系列$990起",
29 | "specialPrice": "$990起",
30 | "oriPrice": "$18500"
31 | },
32 | {
33 | "img": "https://s.yimg.com/zp/MerchandiseImages/B810F61664-Gd-8735341.jpg",
34 | "title": "熱銷印表機單日95折下殺",
35 | "specialPrice": "95折",
36 | "oriPrice": ""
37 | },
38 | {
39 | "img": "https://s.yimg.com/zp/MerchandiseImages/E290A65F00-SP-7909134.jpg",
40 | "title": "BLUE WAY 鼠不盡的好康新品6折起,單筆滿額再折100",
41 | "specialPrice": "$590up",
42 | "oriPrice": "$1380"
43 | },
44 | {
45 | "img": "https://s.yimg.com/zp/MerchandiseImages/FAD93DEE00-SP-7594913.jpg",
46 | "title": "【Philips 飛利浦】LIGHTING LEVER 酷恒LED檯燈 72007 銀色",
47 | "specialPrice": "$799起",
48 | "oriPrice": "$1690"
49 | },
50 | {
51 | "img": "https://s.yimg.com/zp/MerchandiseImages/4A5E9BD839-Gd-7821787.jpg",
52 | "title": "包包快速到貨83折!過年不打烊~結帳享折扣!",
53 | "specialPrice": "83折",
54 | "oriPrice": "$2380"
55 | },
56 | {
57 | "img": "https://s.yimg.com/zp/MerchandiseImages/68A11DC878-SP-7915980.jpg",
58 | "title": "思薇爾 冬季新春福袋2折起,新年挑戰最低價任選89起",
59 | "specialPrice": "2折起",
60 | "oriPrice": "$1480"
61 | }
62 | ]
63 |
--------------------------------------------------------------------------------
/examples/responsive-carousel/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import Carousel from '../../dist/bundle'
4 |
5 | const Container = styled.div`
6 | position: absolute;
7 | top: 0;
8 | left: 0;
9 | min-height: 100%;
10 | width: 100%;
11 | `
12 |
13 | const Row = styled.div`
14 | max-width: 1100px;
15 | padding: 0 50px;
16 | margin: 50px auto;
17 |
18 | @media screen and (max-width: 670px) {
19 | padding: 0;
20 | }
21 | `
22 |
23 | const ArrowBtn = styled.span`
24 | display: inline-block;
25 | position: absolute;
26 | top: 50%;
27 | right: ${({ type }) => (type === 'right' ? '-40px' : 'unset')};
28 | left: ${({ type }) => (type === 'left' ? '-40px' : 'unset')};
29 | width: 45px;
30 | height: 45px;
31 | background: #fff;
32 | border-radius: 50%;
33 | box-shadow: 0 3px 15px rgba(0, 0, 0, 0.15);
34 | cursor: pointer;
35 |
36 | &::after {
37 | content: '';
38 | display: inline-block;
39 | position: absolute;
40 | top: 50%;
41 | left: 50%;
42 | transform: ${({ type }) =>
43 | type === 'right'
44 | ? 'translate(-75%, -50%) rotate(45deg)'
45 | : 'translate(-25%, -50%) rotate(-135deg)'};
46 | width: 10px;
47 | height: 10px;
48 | border-top: 2px solid #666;
49 | border-right: 2px solid #666;
50 | }
51 |
52 | &:hover::after {
53 | border-color: #333;
54 | }
55 | `
56 |
57 | const Card = styled.div`
58 | margin: 2px;
59 | border-radius: 6px;
60 | border: 1px solid #eaeaea;
61 | overflow: hidden;
62 | cursor: pointer;
63 | transition: box-shadow 0.25s;
64 |
65 | :hover {
66 | box-shadow: 0 0 2px 0 #00000063;
67 | }
68 | `
69 |
70 | const Img = styled.div`
71 | height: 160px;
72 | margin-bottom: 4px;
73 | background-image: ${({ img }) => `url(${img})`};
74 | background-position: center;
75 | background-repeat: no-repeat;
76 | background-size: cover;
77 | `
78 |
79 | const Title = styled.div`
80 | margin: 0 10px 10px;
81 | font-size: 15px;
82 | font-weight: bold;
83 | `
84 |
85 | const Star = styled.div`
86 | float: left;
87 | margin: 10px;
88 | color: #26bec9;
89 | font-size: 15px;
90 | `
91 |
92 | const Price = styled.div`
93 | font-size: 12px;
94 | font-weight: 400;
95 | color: #999;
96 | float: right;
97 | margin: 10px;
98 |
99 | span {
100 | font-size: 15px;
101 | color: #26bec9;
102 | }
103 | `
104 |
105 | const Code = styled.pre`
106 | max-width: 1100px;
107 | margin: 15px auto;
108 | background: lightblue;
109 | padding: 20px;
110 | box-sizing: border-box;
111 | overflow: auto;
112 | `
113 |
114 | const Reference = styled.div`
115 | margin: 50px auto;
116 | width: 100%;
117 | max-width: 1100px;
118 | border-top: 1px solid #666;
119 |
120 | img {
121 | width: 100%;
122 | }
123 | `
124 |
125 | const App = () => (
126 |
127 |
128 | Use{' '}
129 |
134 | react-grid-carousel
135 | {' '}
136 | to build responsive carousel
137 |
138 |
139 |
144 | Hit The Slopes
145 |
146 | }
162 | arrowLeft={ }
163 | >
164 | {[...Array(8)].map((_, i) => (
165 |
166 |
167 |
168 |
169 | Day Tour From Tokyo: Tambara Ski Park & Strawberry Picking
170 |
171 | ★★★★★
172 |
173 | TWD 2,500
174 |
175 |
176 |
177 | ))}
178 |
179 |
180 | {` }
196 | arrowLeft={ }
197 | >
198 | {[...Array(8)].map((_, i) => (
199 |
200 |
201 |
202 |
203 | Day Tour From Tokyo: Tambara Ski Park & Strawberry Picking
204 |
205 | ★★★★★
206 |
207 | TWD 2,500
208 |
209 |
210 |
211 | ))}
212 | `}
213 |
214 |
215 | Responsive carousel on{' '}
216 |
221 | KKday
222 |
223 |
224 |
228 |
229 |
230 |
231 |
232 | )
233 |
234 | export default App
235 |
--------------------------------------------------------------------------------
/examples/responsive-carousel/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Responsive carousel
8 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/examples/responsive-carousel/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | ReactDOM.render( , document.getElementById('root'))
6 |
--------------------------------------------------------------------------------
/examples/simple-sample/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import Carousel from '../../dist/bundle'
3 | import styled from 'styled-components'
4 |
5 | const randomImageUrl = 'https://picsum.photos/800/600?random='
6 |
7 | const Item = styled.div`
8 | background-image: ${({ img }) => `url(${img})`};
9 | background-position: center;
10 | background-size: cover;
11 | background-repeat: no-repeat;
12 | width: 100%;
13 | height: 200px;
14 | `
15 |
16 | const App = () => {
17 | const [cols, setCols] = useState(3)
18 | const [rows, setRows] = useState(1)
19 | const [gap, setGap] = useState(10)
20 | const [pages, setPages] = useState(2)
21 |
22 | return (
23 |
24 |
Simple sample
25 |
26 | Cols:{' '}
27 | {
33 | setCols(+e.target.value)
34 | }}
35 | />{' '}
36 | {cols}
37 |
38 |
39 | Rows:{' '}
40 | {
46 | setRows(+e.target.value)
47 | }}
48 | />{' '}
49 | {rows}
50 |
51 |
52 | Pages:{' '}
53 | {
59 | setPages(+e.target.value)
60 | }}
61 | />{' '}
62 | {pages}
63 |
64 |
65 | Gap:{' '}
66 | {
72 | setGap(+e.target.value)
73 | }}
74 | />{' '}
75 | {gap}
76 |
77 |
78 |
79 | {[...Array(cols * rows * pages)].map((_, i) => (
80 |
81 |
82 |
83 | ))}
84 |
85 | Photo by{' '}
86 |
91 | https://picsum.photos/
92 |
93 |
94 | )
95 | }
96 |
97 | export default App
98 |
--------------------------------------------------------------------------------
/examples/simple-sample/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Simple sample
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/examples/simple-sample/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | ReactDOM.render( , document.getElementById('root'))
6 |
--------------------------------------------------------------------------------
/examples/testimonial-carousel/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import Carousel from '../../dist/bundle'
4 | import testimonials from './mockTestimonialList.json'
5 |
6 | const Container = styled.div`
7 | background: #f7f8fa;
8 | position: absolute;
9 | top: 0;
10 | left: 0;
11 | width: 100%;
12 | min-height: 100%;
13 | `
14 |
15 | const Card = styled.div`
16 | background: #fff;
17 | border-radius: 3px;
18 | box-shadow: 0 1px 3px 0 rgba(20, 23, 28, 0.15);
19 | height: 100%;
20 | min-height: 240px;
21 | padding: 32px;
22 | margin: 5px;
23 | `
24 | const User = styled.div`
25 | display: flex;
26 | `
27 | const Avatar = styled.div`
28 | width: 64px;
29 | height: 64px;
30 | background: rgb(104, 111, 122);
31 | border-radius: 50%;
32 | align-items: center;
33 | justify-content: center;
34 | color: #fff;
35 | display: flex;
36 | font-weight: bold;
37 | margin-right: 10px;
38 | `
39 |
40 | const Username = styled.div`
41 | display: flex;
42 | align-items: center;
43 | `
44 |
45 | const Text = styled.div`
46 | margin-top: 20px;
47 | font-size: 16px;
48 | color: #333;
49 | `
50 |
51 | const Code = styled.pre`
52 | max-width: 1300px;
53 | margin: 15px auto;
54 | background: #fff;
55 | padding: 20px;
56 | box-sizing: border-box;
57 | overflow: auto;
58 | `
59 |
60 | const Reference = styled.div`
61 | margin: 50px auto;
62 | width: 100%;
63 | max-width: 1300px;
64 | border-top: 1px solid #666;
65 |
66 | img {
67 | width: 100%;
68 | }
69 | `
70 |
71 | const App = () => {
72 | return (
73 |
74 |
75 | Use{' '}
76 |
81 | react-grid-carousel
82 | {' '}
83 | to build testimonial carousel
84 |
85 |
89 | {testimonials.map(({ name, text }, i) => (
90 |
91 |
92 |
93 | {name[0]}
94 | {name}
95 |
96 | {text}
97 |
98 |
99 | ))}
100 |
101 | {`
105 | {testimonials.map(({ name, text }, i) => (
106 |
107 |
108 |
109 | {name[0]}
110 | {name}
111 |
112 | {text}
113 |
114 |
115 | ))}
116 | `}
117 |
118 |
119 | Testimonial carousel on{' '}
120 |
125 | Udemy
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | )
134 | }
135 |
136 | export default App
137 |
--------------------------------------------------------------------------------
/examples/testimonial-carousel/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Testimonial carousel
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/examples/testimonial-carousel/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | ReactDOM.render( , document.getElementById('root'))
6 |
--------------------------------------------------------------------------------
/examples/testimonial-carousel/mockTestimonialList.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Borivoje",
4 | "text": "Udemy is a life saver. I don't have the time or money for a college education. My goal is to become a freelance web developer, and thanks to Udemy, I'm really close."
5 | },
6 | {
7 | "name": "Dipesh",
8 | "text": "I believe in lifelong learning and Udemy is a great place to learn from experts. I've learned a lot and recommend it to all my friends."
9 | },
10 | {
11 | "name": "Kathy",
12 | "text": "My children and I LOVE Udemy! The courses are fantastic and the instructors are so fun and knowledgeable. I only wish we found it sooner."
13 | },
14 | {
15 | "name": "Zulaika",
16 | "text": "I work in project management and joined Udemy because I get great courses for less. The instructors are fantastic, interesting, and helpful. I plan to use Udemy for a long time!"
17 | },
18 | {
19 | "name": "Marco",
20 | "text": "Thank you Udemy! You've renewed my passion for learning and my dream of becoming a web developer."
21 | },
22 | {
23 | "name": "Justin",
24 | "text": "The best part about Udemy is the selection. You can find a course for anything you want to learn!"
25 | }
26 | ]
27 |
--------------------------------------------------------------------------------
/examples/tour-carousel/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import Carousel from '../../dist/bundle'
4 | import cities from './cities.json'
5 |
6 | const Container = styled.div`
7 | background: #f5f5f5;
8 | position: absolute;
9 | top: 0;
10 | left: 0;
11 | min-height: 100%;
12 | width: 100%;
13 | `
14 |
15 | const Row = styled.div`
16 | max-width: 1200px;
17 | margin: 50px auto;
18 | `
19 |
20 | const ArrowBtn = styled.span`
21 | display: inline-block;
22 | position: absolute;
23 | top: 50%;
24 | right: ${({ type }) => (type === 'right' ? '8px' : 'unset')};
25 | left: ${({ type }) => (type === 'left' ? '8px' : 'unset')};
26 | transform: ${({ type }) =>
27 | `translateY(-50%) rotate(${type === 'right' ? '45deg' : '-135deg'})`};
28 | width: 16px;
29 | height: 16px;
30 | cursor: pointer;
31 | border-top: 2px solid #888;
32 | border-right: 2px solid #888;
33 |
34 | &:hover {
35 | border-color: #333;
36 | }
37 | `
38 |
39 | const City = styled.div`
40 | background-image: ${({ img }) => `url(${img})`};
41 | background-size: cover;
42 | background-position: center;
43 | background-repeat: no-repeat;
44 | height: 220px;
45 | line-height: 220px;
46 | font-size: 24px;
47 | font-weight: bold;
48 | color: #fff;
49 | text-align: center;
50 | margin: 7px;
51 | box-shadow: 0 0 2px 0 #666;
52 | border-radius: 3px;
53 | cursor: pointer;
54 | transition: transform 0.25s, box-shadow 0.25s;
55 |
56 | &:hover {
57 | transform: translateY(-5px);
58 | box-shadow: 0 5px 5px 0 #666;
59 | }
60 | `
61 |
62 | const Code = styled.pre`
63 | max-width: 1200px;
64 | margin: 15px auto;
65 | background: #fff;
66 | padding: 20px;
67 | box-sizing: border-box;
68 | overflow: auto;
69 | `
70 |
71 | const Reference = styled.div`
72 | margin: 50px auto;
73 | width: 100%;
74 | max-width: 1200px;
75 | border-top: 1px solid #666;
76 |
77 | img {
78 | width: 100%;
79 | }
80 | `
81 |
82 | const App = () => (
83 |
84 |
85 | Use{' '}
86 |
91 | react-grid-carousel
92 | {' '}
93 | to build tour carousel
94 |
95 |
96 |
101 | TOP DESTINATIONS
102 |
103 | }
108 | arrowRight={ }
109 | >
110 | {cities.map((city, i) => (
111 |
112 | {city.name}
113 |
114 | ))}
115 |
116 |
117 | {` }
122 | arrowRight={ }
123 | >
124 | {cities.map((city, i) => (
125 |
126 | {city.name}
127 |
128 | ))}
129 | `}
130 |
131 |
132 | Tour carousel on{' '}
133 |
138 | KLOOK
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 | )
147 |
148 | export default App
149 |
--------------------------------------------------------------------------------
/examples/tour-carousel/cities.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Hong Kong",
4 | "img": "https://res.klook.com/image/upload/cities/jrfyzvgzvhs1iylduuhj.jpg"
5 | },
6 | {
7 | "name": "Macau",
8 | "img": "https://res.klook.com/image/upload/cities/c1cklkyp6ms02tougufx.jpg"
9 | },
10 | {
11 | "name": "Singapore",
12 | "img": "https://res.klook.com/image/upload/cities/jv8g79dw3j5fmi9qozoa.jpg"
13 | },
14 | {
15 | "name": "Seoul",
16 | "img": "https://res.klook.com/image/upload/cities/hlx7brfs0u9k2unjhmr0.jpg"
17 | },
18 | {
19 | "name": "Beijing",
20 | "img": "https://res.klook.com/image/upload/cities/br3mng95h81qi71dqpvk.jpg"
21 | },
22 | {
23 | "name": "Tokyo",
24 | "img": "https://res.klook.com/image/upload/cities/xvp6saafvo0aeykw3di0.jpg"
25 | },
26 | {
27 | "name": "Osaka",
28 | "img": "https://res.klook.com/image/upload/cities/migfkd37vdt8xhuenn0s.jpg"
29 | },
30 | {
31 | "name": "JR Pass",
32 | "img": "https://res.klook.com/image/upload/cities/vplgtbmehzxlzvdbnpby.jpg"
33 | },
34 | {
35 | "name": "Okinawa & Ishigaki",
36 | "img": "https://res.klook.com/image/upload/cities/e8fnw35p6zgusq218foj.jpg"
37 | },
38 | {
39 | "name": "Taipei",
40 | "img": "https://res.klook.com/image/upload/cities/i5itbqsg2alwruhqkgvx.jpg"
41 | },
42 | {
43 | "name": "Bangkok",
44 | "img": "https://res.klook.com/image/upload/cities/wbulbfghbwsuvelgvibn.jpg"
45 | },
46 | {
47 | "name": "Phuket",
48 | "img": "https://res.klook.com/image/upload/cities/ibudlvrsfnlck3sgounc.jpg"
49 | },
50 | {
51 | "name": "Bali",
52 | "img": "https://res.klook.com/image/upload/cities/zrvzvz42wh91resimuyz.jpg"
53 | },
54 | {
55 | "name": "London",
56 | "img": "https://res.klook.com/image/upload/cities/xkeic9zojhtxffeovjtl.jpg"
57 | },
58 | {
59 | "name": "Paris",
60 | "img": "https://res.klook.com/image/upload/cities/bqi3forinxbwjuknp04v.jpg"
61 | },
62 | {
63 | "name": "New York",
64 | "img": "https://res.klook.com/image/upload/cities/liw377az16sxmp9a6ylg.jpg"
65 | }
66 | ]
67 |
--------------------------------------------------------------------------------
/examples/tour-carousel/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Tour carousel
8 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/examples/tour-carousel/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | ReactDOM.render( , document.getElementById('root'))
6 |
--------------------------------------------------------------------------------
/examples/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const fs = require('fs')
3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin')
4 |
5 | function isDirectory(dir) {
6 | return fs.lstatSync(dir).isDirectory()
7 | }
8 |
9 | module.exports = {
10 | mode: 'development',
11 | entry: fs.readdirSync(__dirname).reduce((entries, dir) => {
12 | if (dir !== 'dist' && isDirectory(path.join(__dirname, dir))) {
13 | entries[dir] = path.join(__dirname, dir, 'index.js')
14 | }
15 |
16 | return entries
17 | }, {}),
18 | output: {
19 | path: path.resolve(__dirname, 'dist'),
20 | filename: '[name].js',
21 | chunkFilename: '[id].chunk.js'
22 | },
23 | module: {
24 | rules: [
25 | {
26 | test: /\.js$/,
27 | exclude: /node_modules/,
28 | use: {
29 | loader: 'babel-loader'
30 | }
31 | }
32 | ]
33 | },
34 | plugins: [
35 | new CleanWebpackPlugin({
36 | verbose: true
37 | })
38 | ],
39 | devServer: {
40 | contentBase: __dirname,
41 | publicPath: '/dist/',
42 | compress: true,
43 | hot: true,
44 | inline: true,
45 | host: '0.0.0.0'
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-grid-carousel",
3 | "version": "1.0.1",
4 | "description": "React resposive carousel component w/ grid layout",
5 | "homepage": "https://react-grid-carousel.now.sh/",
6 | "keywords": [
7 | "react",
8 | "carousel",
9 | "slider",
10 | "gallery",
11 | "image",
12 | "grid",
13 | "responsive",
14 | "react-component",
15 | "react-carousel",
16 | "react-slider",
17 | "react-image",
18 | "react-grid"
19 | ],
20 | "repository": {
21 | "url": "git@github.com:x3388638/react-grid-carousel.git",
22 | "type": "git"
23 | },
24 | "main": "dist/bundle.js",
25 | "scripts": {
26 | "dev": "npm run build && webpack-dev-server --config examples/webpack.config.js",
27 | "build": "rollup -c",
28 | "prettier:check": "prettier --check './**/*.{js,json,css}' && echo \"✅ Prettier validated\"",
29 | "prettier:write": "prettier --write './**/*.{js,json,css}'",
30 | "stylelint": "stylelint './{src,examples,stories}/**/*.js' && echo \"✅ Stylelint validated\"",
31 | "lint": "eslint './**/*.js'",
32 | "lint:fix": "eslint './**/*.js' --fix",
33 | "storybook": "start-storybook -p 6006",
34 | "build-storybook": "build-storybook",
35 | "deploy:now": "webpack --config examples/webpack.config.js && now examples/ -n react-grid-carousel --prod"
36 | },
37 | "author": "YY",
38 | "license": "MIT",
39 | "dependencies": {
40 | "lodash.debounce": "^4.0.8",
41 | "prop-types": "^15.7.2",
42 | "smoothscroll-polyfill": "^0.4.4",
43 | "styled-components": "^4.4.1"
44 | },
45 | "devDependencies": {
46 | "@babel/core": "^7.8.3",
47 | "@babel/preset-env": "^7.8.3",
48 | "@babel/preset-react": "^7.8.3",
49 | "@storybook/addon-actions": "^5.3.8",
50 | "@storybook/addon-knobs": "^5.3.8",
51 | "@storybook/addon-links": "^5.3.8",
52 | "@storybook/addon-viewport": "^5.3.8",
53 | "@storybook/addons": "^5.3.8",
54 | "@storybook/react": "^5.3.8",
55 | "babel-loader": "^8.0.6",
56 | "babel-plugin-styled-components": "^1.10.6",
57 | "clean-webpack-plugin": "^3.0.0",
58 | "eslint": "^6.8.0",
59 | "eslint-plugin-react": "^7.18.0",
60 | "eslint-plugin-react-hooks": "^2.3.0",
61 | "husky": "^4.0.10",
62 | "lint-staged": "^10.0.1",
63 | "prettier": "^1.19.1",
64 | "react": "^16.12.0",
65 | "react-dom": "^16.12.0",
66 | "rollup": "^1.29.1",
67 | "rollup-plugin-babel": "^4.3.3",
68 | "stylelint": "^13.2.0",
69 | "stylelint-config-prettier": "^8.0.1",
70 | "stylelint-config-standard": "^20.0.0",
71 | "stylelint-config-styled-components": "^0.1.1",
72 | "stylelint-processor-styled-components": "^1.10.0",
73 | "webpack": "^4.41.5",
74 | "webpack-cli": "^3.3.10",
75 | "webpack-dev-server": "^3.10.1"
76 | },
77 | "peerDependencies": {
78 | "react": "^16.12.0",
79 | "react-dom": "^16.12.0"
80 | },
81 | "lint-staged": {
82 | "*.{js,json,css}": [
83 | "npm run prettier:check"
84 | ],
85 | "*.js": [
86 | "npm run stylelint",
87 | "npm run lint"
88 | ]
89 | },
90 | "husky": {
91 | "hooks": {
92 | "pre-commit": "lint-staged"
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel'
2 | import pkg from './package.json'
3 |
4 | export default [
5 | {
6 | input: 'src/app.js',
7 | plugins: [
8 | babel({
9 | exclude: 'node_modules/**'
10 | })
11 | ],
12 | output: {
13 | file: pkg.main,
14 | format: 'cjs'
15 | },
16 | external: [
17 | ...Object.keys(pkg.dependencies),
18 | ...Object.keys(pkg.peerDependencies)
19 | ]
20 | }
21 | ]
22 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | import Carousel from './components/Carousel'
2 |
3 | export default Carousel
4 |
--------------------------------------------------------------------------------
/src/components/ArrowButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import styled from 'styled-components'
4 |
5 | const ButtonWrapper = styled.div`
6 | @media screen and (max-width: ${({ mobileBreakpoint }) =>
7 | mobileBreakpoint}px) {
8 | display: none;
9 | }
10 | `
11 |
12 | const Button = styled.span`
13 | position: absolute;
14 | top: calc(50% - 17.5px);
15 | height: 35px;
16 | width: 35px;
17 | background: #fff;
18 | border-radius: 50%;
19 | box-shadow: 0 0 5px 0 #0009;
20 | z-index: 10;
21 | cursor: pointer;
22 | font-size: 10px;
23 | opacity: 0.6;
24 | transition: opacity 0.25s;
25 | left: ${({ type }) => (type === 'prev' ? '5px' : 'initial')};
26 | right: ${({ type }) => (type === 'next' ? '5px' : 'initial')};
27 |
28 | &:hover {
29 | opacity: 1;
30 | }
31 |
32 | &::before {
33 | content: '';
34 | height: 10px;
35 | width: 10px;
36 | background: transparent;
37 | border-top: 2px solid #000;
38 | border-right: 2px solid #000;
39 | display: inline-block;
40 | position: absolute;
41 | top: 50%;
42 | left: 50%;
43 | transform: ${({ type }) =>
44 | type === 'prev'
45 | ? 'translate(-25%, -50%) rotate(-135deg)'
46 | : 'translate(-75%, -50%) rotate(45deg)'};
47 | }
48 | `
49 |
50 | const ArrowButton = ({
51 | type,
52 | mobileBreakpoint = 1,
53 | hidden = false,
54 | CustomBtn,
55 | onClick
56 | }) => (
57 |
62 | {CustomBtn ? (
63 | typeof CustomBtn === 'function' ? (
64 |
65 | ) : (
66 | CustomBtn
67 | )
68 | ) : (
69 |
70 | )}
71 |
72 | )
73 |
74 | ArrowButton.propTypes = {
75 | type: PropTypes.oneOf(['prev', 'next']).isRequired,
76 | mobileBreakpoint: PropTypes.number,
77 | hidden: PropTypes.bool,
78 | CustomBtn: PropTypes.oneOfType([
79 | PropTypes.node,
80 | PropTypes.element,
81 | PropTypes.elementType
82 | ]),
83 | onClick: PropTypes.func.isRequired
84 | }
85 |
86 | export default ArrowButton
87 |
--------------------------------------------------------------------------------
/src/components/Carousel.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react'
2 | import PropTypes from 'prop-types'
3 | import styled from 'styled-components'
4 | import ArrowButton from './ArrowButton'
5 | import Dot from './Dot'
6 | import smoothscroll from 'smoothscroll-polyfill'
7 | import useResponsiveLayout from '../hooks/responsiveLayoutHook'
8 | import { addResizeHandler, removeResizeHandler } from '../utils/resizeListener'
9 |
10 | const Container = styled.div`
11 | position: relative;
12 | `
13 |
14 | const RailWrapper = styled.div`
15 | overflow: hidden;
16 | margin: ${({ showDots }) => (showDots ? '0 20px 15px 20px' : '0 20px')};
17 |
18 | @media screen and (max-width: ${({ mobileBreakpoint }) =>
19 | mobileBreakpoint}px) {
20 | overflow-x: auto;
21 | margin: 0;
22 | scroll-snap-type: ${({ scrollSnap }) => (scrollSnap ? 'x mandatory' : '')};
23 | scrollbar-width: none;
24 |
25 | &::-webkit-scrollbar {
26 | display: none;
27 | }
28 | }
29 | `
30 |
31 | const Rail = styled.div`
32 | display: grid;
33 | grid-column-gap: ${({ gap }) => `${gap}px`};
34 | position: relative;
35 | transition: transform 0.5s cubic-bezier(0.2, 1, 0.3, 1) 0s;
36 | grid-template-columns: ${({ page }) => `repeat(${page}, 100%)`};
37 | transform: ${({ currentPage, gap }) =>
38 | `translateX(calc(${-100 * currentPage}% - ${gap * currentPage}px))`};
39 |
40 | @media screen and (max-width: ${({ mobileBreakpoint }) =>
41 | mobileBreakpoint}px) {
42 | padding-left: ${({ gap }) => `${gap}px`};
43 | grid-template-columns: ${({ page }) => `repeat(${page}, 90%)`};
44 | grid-column-gap: ${({ cols, rows, gap }) =>
45 | `calc(${(cols * rows - 1) * 90}% + ${cols * rows * gap}px)`};
46 | transform: translateX(0);
47 | }
48 | `
49 |
50 | const ItemSet = styled.div`
51 | display: grid;
52 | grid-template-columns: ${({ cols }) => `repeat(${cols}, 1fr)`};
53 | grid-template-rows: ${({ rows }) => `repeat(${rows}, 1fr)`};
54 | grid-gap: ${({ gap }) => `${gap}px`};
55 |
56 | @media screen and (max-width: ${({ mobileBreakpoint }) =>
57 | mobileBreakpoint}px) {
58 | grid-template-columns: ${({ cols, rows }) =>
59 | `repeat(${cols * rows}, 100%)`};
60 | grid-template-rows: 1fr;
61 |
62 | &:last-of-type > ${/* sc-sel */ Item}:last-of-type {
63 | padding-right: ${({ gap }) => `${gap}px`};
64 | margin-right: ${({ gap }) => `-${gap}px`};
65 | }
66 | }
67 | `
68 |
69 | const Dots = styled.div`
70 | position: absolute;
71 | display: flex;
72 | align-items: center;
73 | justify-content: center;
74 | bottom: -12px;
75 | height: 10px;
76 | width: 100%;
77 | line-height: 10px;
78 | text-align: center;
79 |
80 | @media screen and (max-width: ${({ mobileBreakpoint }) =>
81 | mobileBreakpoint}px) {
82 | display: none;
83 | }
84 | `
85 |
86 | const Item = styled.div`
87 | scroll-snap-align: ${({ scrollSnap }) => (scrollSnap ? 'center' : '')};
88 | `
89 |
90 | const CAROUSEL_ITEM = 'CAROUSEL_ITEM'
91 | const Carousel = ({
92 | cols: colsProp = 1,
93 | rows: rowsProp = 1,
94 | gap: gapProp = 10,
95 | loop: loopProp = false,
96 | scrollSnap = true,
97 | hideArrow = false,
98 | showDots = false,
99 | autoplay: autoplayProp,
100 | dotColorActive = '#795548',
101 | dotColorInactive = '#ccc',
102 | responsiveLayout,
103 | mobileBreakpoint = 767,
104 | arrowLeft,
105 | arrowRight,
106 | dot,
107 | containerClassName = '',
108 | containerStyle = {},
109 | children
110 | }) => {
111 | const [currentPage, setCurrentPage] = useState(0)
112 | const [isHover, setIsHover] = useState(false)
113 | const [isTouch, setIsTouch] = useState(false)
114 | const [cols, setCols] = useState(colsProp)
115 | const [rows, setRows] = useState(rowsProp)
116 | const [gap, setGap] = useState(0)
117 | const [loop, setLoop] = useState(loopProp)
118 | const [autoplay, setAutoplay] = useState(autoplayProp)
119 | const [railWrapperWidth, setRailWrapperWidth] = useState(0)
120 | const [hasSetResizeHandler, setHasSetResizeHandler] = useState(false)
121 | const railWrapperRef = useRef(null)
122 | const autoplayIntervalRef = useRef(null)
123 | const breakpointSetting = useResponsiveLayout(responsiveLayout)
124 | const randomKey = useMemo(() => `${Math.random()}-${Math.random()}`, [])
125 |
126 | useEffect(() => {
127 | smoothscroll.polyfill()
128 | }, [])
129 |
130 | useEffect(() => {
131 | const { cols, rows, gap, loop, autoplay } = breakpointSetting || {}
132 | setCols(cols || colsProp)
133 | setRows(rows || rowsProp)
134 | setGap(parseGap(gap || gapProp))
135 | setLoop(loop || loopProp)
136 | setAutoplay(autoplay || autoplayProp)
137 | setCurrentPage(0)
138 | }, [
139 | breakpointSetting,
140 | colsProp,
141 | rowsProp,
142 | gapProp,
143 | loopProp,
144 | autoplayProp,
145 | parseGap
146 | ])
147 |
148 | const handleRailWrapperResize = useCallback(() => {
149 | railWrapperRef.current &&
150 | setRailWrapperWidth(railWrapperRef.current.offsetWidth)
151 | }, [railWrapperRef])
152 |
153 | const setResizeHandler = useCallback(() => {
154 | addResizeHandler(`gapCalculator-${randomKey}`, handleRailWrapperResize)
155 | setHasSetResizeHandler(true)
156 | }, [randomKey, handleRailWrapperResize])
157 |
158 | const rmResizeHandler = useCallback(() => {
159 | removeResizeHandler(`gapCalculator-${randomKey}`)
160 | setHasSetResizeHandler(false)
161 | }, [randomKey])
162 |
163 | const parseGap = useCallback(
164 | gap => {
165 | let parsed = gap
166 | let shouldSetResizeHandler = false
167 |
168 | if (typeof gap !== 'number') {
169 | switch (/\D*$/.exec(gap)[0]) {
170 | case 'px': {
171 | parsed = +gap.replace('px', '')
172 | break
173 | }
174 | case '%': {
175 | let wrapperWidth =
176 | railWrapperWidth || railWrapperRef.current
177 | ? railWrapperRef.current.offsetWidth
178 | : 0
179 |
180 | parsed = (wrapperWidth * gap.replace('%', '')) / 100
181 | shouldSetResizeHandler = true
182 | break
183 | }
184 | default: {
185 | parsed = 0
186 | console.error(
187 | `Doesn't support the provided measurement unit: ${gap}`
188 | )
189 | }
190 | }
191 | }
192 |
193 | shouldSetResizeHandler && !hasSetResizeHandler && setResizeHandler()
194 | !shouldSetResizeHandler && hasSetResizeHandler && rmResizeHandler()
195 | return parsed
196 | },
197 | [
198 | railWrapperWidth,
199 | railWrapperRef,
200 | hasSetResizeHandler,
201 | setResizeHandler,
202 | rmResizeHandler
203 | ]
204 | )
205 |
206 | const itemList = useMemo(
207 | () =>
208 | React.Children.toArray(children).filter(
209 | child => child.type.displayName === CAROUSEL_ITEM
210 | ),
211 | [children]
212 | )
213 |
214 | const itemAmountPerSet = cols * rows
215 | const itemSetList = useMemo(
216 | () =>
217 | itemList.reduce((result, item, i) => {
218 | const itemComponent = (
219 | -
220 | {item}
221 |
222 | )
223 |
224 | if (i % itemAmountPerSet === 0) {
225 | result.push([itemComponent])
226 | } else {
227 | result[result.length - 1].push(itemComponent)
228 | }
229 |
230 | return result
231 | }, []),
232 | [itemList, itemAmountPerSet, scrollSnap]
233 | )
234 |
235 | const page = Math.ceil(itemList.length / itemAmountPerSet)
236 |
237 | const handlePrev = useCallback(() => {
238 | setCurrentPage(p => {
239 | const prevPage = p - 1
240 | if (loop && prevPage < 0) {
241 | return page - 1
242 | }
243 |
244 | return prevPage
245 | })
246 | }, [loop, page])
247 |
248 | const handleNext = useCallback(
249 | (isMobile = false) => {
250 | const railWrapper = railWrapperRef.current
251 | if (isMobile && railWrapper) {
252 | if (!scrollSnap) {
253 | return
254 | }
255 |
256 | const { scrollLeft, offsetWidth, scrollWidth } = railWrapper
257 | railWrapper.scrollBy({
258 | top: 0,
259 | left:
260 | loop && scrollLeft + offsetWidth >= scrollWidth
261 | ? -scrollLeft
262 | : scrollLeft === 0
263 | ? gap +
264 | (offsetWidth - gap) * 0.9 -
265 | (offsetWidth * 0.1 - gap * 1.1) / 2
266 | : (offsetWidth - gap) * 0.9 + gap,
267 | behavior: 'smooth'
268 | })
269 | } else {
270 | setCurrentPage(p => {
271 | const nextPage = p + 1
272 | if (nextPage >= page) {
273 | return loop ? 0 : p
274 | }
275 |
276 | return nextPage
277 | })
278 | }
279 | },
280 | [loop, page, gap, railWrapperRef, scrollSnap]
281 | )
282 |
283 | const startAutoplayInterval = useCallback(() => {
284 | if (autoplayIntervalRef.current === null && typeof autoplay === 'number') {
285 | autoplayIntervalRef.current = setInterval(() => {
286 | handleNext(window.innerWidth <= mobileBreakpoint)
287 | }, autoplay)
288 | }
289 | }, [autoplay, autoplayIntervalRef, handleNext, mobileBreakpoint])
290 |
291 | useEffect(() => {
292 | startAutoplayInterval()
293 |
294 | return () => {
295 | if (autoplayIntervalRef.current !== null) {
296 | clearInterval(autoplayIntervalRef.current)
297 | autoplayIntervalRef.current = null
298 | }
299 | }
300 | }, [startAutoplayInterval, autoplayIntervalRef])
301 |
302 | useEffect(() => {
303 | if (isHover || isTouch) {
304 | clearInterval(autoplayIntervalRef.current)
305 | autoplayIntervalRef.current = null
306 | } else {
307 | startAutoplayInterval()
308 | }
309 | }, [isHover, isTouch, autoplayIntervalRef, startAutoplayInterval])
310 |
311 | const turnToPage = useCallback(page => {
312 | setCurrentPage(page)
313 | }, [])
314 |
315 | const handleHover = useCallback(() => {
316 | setIsHover(hover => !hover)
317 | }, [])
318 |
319 | const handleTouch = useCallback(() => {
320 | setIsTouch(touch => !touch)
321 | }, [])
322 |
323 | return (
324 |
332 |
339 |
345 |
353 | {itemSetList.map((set, i) => (
354 |
361 | {set}
362 |
363 | ))}
364 |
365 |
366 | {showDots && (
367 |
368 | {[...Array(page)].map((_, i) => (
369 |
378 | ))}
379 |
380 | )}
381 |
388 |
389 | )
390 | }
391 |
392 | const positiveNumberValidator = (props, propName, componentName) => {
393 | const prop = props[propName]
394 | if ((prop !== undefined && typeof prop !== 'number') || prop <= 0) {
395 | return new Error(
396 | `Invalid prop \`${propName}\`: ${props[propName]} supplied to \`${componentName}\`. expected positive \`number\``
397 | )
398 | }
399 | }
400 |
401 | Carousel.propTypes = {
402 | cols: positiveNumberValidator,
403 | rows: positiveNumberValidator,
404 | gap: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
405 | loop: PropTypes.bool,
406 | scrollSnap: PropTypes.bool,
407 | hideArrow: PropTypes.bool,
408 | showDots: PropTypes.bool,
409 | autoplay: PropTypes.number,
410 | dotColorActive: PropTypes.string,
411 | dotColorInactive: PropTypes.string,
412 | responsiveLayout: PropTypes.arrayOf(
413 | PropTypes.shape({
414 | breakpoint: PropTypes.number.isRequired,
415 | cols: positiveNumberValidator,
416 | rows: positiveNumberValidator,
417 | gap: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
418 | loop: PropTypes.bool,
419 | autoplay: PropTypes.number
420 | })
421 | ),
422 | mobileBreakpoint: PropTypes.number,
423 | arrowLeft: PropTypes.oneOfType([
424 | PropTypes.node,
425 | PropTypes.element,
426 | PropTypes.elementType
427 | ]),
428 | arrowRight: PropTypes.oneOfType([
429 | PropTypes.node,
430 | PropTypes.element,
431 | PropTypes.elementType
432 | ]),
433 | dot: PropTypes.oneOfType([
434 | PropTypes.node,
435 | PropTypes.element,
436 | PropTypes.elementType
437 | ]),
438 | containerClassName: PropTypes.string,
439 | containerStyle: PropTypes.object
440 | }
441 |
442 | Carousel.Item = ({ children }) => children
443 | Carousel.Item.displayName = CAROUSEL_ITEM
444 | export default Carousel
445 |
--------------------------------------------------------------------------------
/src/components/Dot.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react'
2 | import PropTypes from 'prop-types'
3 | import styled from 'styled-components'
4 |
5 | const DotWrapper = styled.div`
6 | display: flex;
7 | margin: 0 5px;
8 | cursor: pointer;
9 | `
10 |
11 | const DotDefault = styled.div`
12 | width: 8px;
13 | height: 8px;
14 | border-radius: 50%;
15 | background: ${({ color }) => color};
16 | `
17 |
18 | const Dot = ({
19 | index,
20 | isActive = false,
21 | dotColorInactive,
22 | dotColorActive,
23 | dot: DotCustom,
24 | onClick
25 | }) => {
26 | const handleClick = useCallback(() => {
27 | onClick(index)
28 | }, [index, onClick])
29 |
30 | return (
31 |
32 | {DotCustom ? (
33 |
34 | ) : (
35 |
36 | )}
37 |
38 | )
39 | }
40 |
41 | Dot.propTypes = {
42 | index: PropTypes.number.isRequired,
43 | isActive: PropTypes.bool,
44 | dotColorInactive: PropTypes.string,
45 | dotColorActive: PropTypes.string,
46 | dot: PropTypes.oneOfType([
47 | PropTypes.node,
48 | PropTypes.element,
49 | PropTypes.elementType
50 | ]),
51 | onClick: PropTypes.func.isRequired
52 | }
53 |
54 | export default Dot
55 |
--------------------------------------------------------------------------------
/src/hooks/responsiveLayoutHook.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useCallback, useState, useMemo } from 'react'
2 | import { addResizeHandler, removeResizeHandler } from '../utils/resizeListener'
3 |
4 | const useResponsiveLayout = (breakpointList = []) => {
5 | const [currentBreakpointSetting, setCurrentBreakpointSetting] = useState()
6 | const random = useMemo(() => `${Math.random()}-${Math.random()}`, [])
7 |
8 | const sortedBreakpointList = useMemo(
9 | () =>
10 | [...breakpointList].sort(
11 | (a, b) => (b.breakpoint || 0) - (a.breakpoint || 0)
12 | ),
13 | [breakpointList]
14 | )
15 |
16 | const handleResize = useCallback(() => {
17 | const windowWidth = window.innerWidth
18 | let matchedSetting
19 |
20 | sortedBreakpointList.find(setting => {
21 | if (windowWidth <= setting.breakpoint) {
22 | matchedSetting = setting
23 | } else {
24 | return true
25 | }
26 | })
27 |
28 | setCurrentBreakpointSetting(matchedSetting)
29 | }, [sortedBreakpointList])
30 |
31 | useEffect(() => {
32 | if (breakpointList.length) {
33 | handleResize()
34 | addResizeHandler(`responsiveLayout-${random}`, handleResize)
35 |
36 | return () => {
37 | removeResizeHandler(`responsiveLayout-${random}`)
38 | }
39 | }
40 | }, [breakpointList, handleResize, random])
41 |
42 | return currentBreakpointSetting
43 | }
44 |
45 | export default useResponsiveLayout
46 |
--------------------------------------------------------------------------------
/src/utils/resizeListener.js:
--------------------------------------------------------------------------------
1 | import debounce from 'lodash.debounce'
2 | const HANDLER_NAME_SPACE = '__react-grid-carousle-resize-handler'
3 |
4 | const handleResize = debounce(e => {
5 | Object.values(window[HANDLER_NAME_SPACE]).forEach(handler => {
6 | if (typeof handler === 'function') {
7 | handler(e)
8 | }
9 | })
10 | }, 16)
11 |
12 | const setupListener = () => {
13 | window.addEventListener('resize', handleResize)
14 | }
15 |
16 | const removeListener = () => {
17 | window.removeEventListener('resize', handleResize)
18 | }
19 |
20 | export const addResizeHandler = (key, handler) => {
21 | if (typeof window[HANDLER_NAME_SPACE] !== 'object') {
22 | window[HANDLER_NAME_SPACE] = {}
23 | setupListener()
24 | }
25 |
26 | window[HANDLER_NAME_SPACE][key] = handler
27 | }
28 |
29 | export const removeResizeHandler = key => {
30 | delete window[HANDLER_NAME_SPACE][key]
31 |
32 | if (!Object.keys(window[HANDLER_NAME_SPACE])) {
33 | delete window[HANDLER_NAME_SPACE]
34 | removeListener()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/stories/Carousel.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from 'react'
2 | import Carousel from '../src/components/Carousel'
3 | import styled from 'styled-components'
4 | import { withKnobs, number, boolean } from '@storybook/addon-knobs'
5 |
6 | const randomImageUrl = 'https://picsum.photos/800/600?random='
7 |
8 | const Item = styled.div`
9 | height: 300px;
10 | border-radius: 8px;
11 | background-image: ${({ img }) => `url(${img})`};
12 | background-position: center;
13 | background-size: cover;
14 | background-repeat: no-repeat;
15 | `
16 |
17 | export const SingleColumn = () => {
18 | return (
19 |
20 | {[...Array(5)].map((_, i) => (
21 |
22 |
23 |
24 | ))}
25 |
26 | )
27 | }
28 |
29 | export const MultiColumns = () => {
30 | return (
31 |
32 | {[...Array(8)].map((_, i) => (
33 |
34 |
35 |
36 | ))}
37 |
38 | )
39 | }
40 |
41 | export const MultiRows = () => {
42 | return (
43 |
44 | {[...Array(25)].map((_, i) => (
45 |
46 |
47 |
48 | ))}
49 |
50 | )
51 | }
52 |
53 | export const WithDots = () => {
54 | return (
55 |
56 | {[...Array(9)].map((_, i) => (
57 |
58 |
59 |
60 | ))}
61 |
62 | )
63 | }
64 |
65 | export const ShowArrowOnHover = () => {
66 | const [showArrow, setShowArrow] = useState(false)
67 |
68 | const handleHover = useCallback(() => {
69 | setShowArrow(show => !show)
70 | }, [])
71 |
72 | return (
73 |
74 |
75 | {[...Array(6)].map((_, i) => (
76 |
77 |
78 |
79 | ))}
80 |
81 |
82 | )
83 | }
84 |
85 | export const Autoplay = () => {
86 | return (
87 |
88 | {[...Array(15)].map((_, i) => (
89 |
90 |
91 |
92 | ))}
93 |
94 | )
95 | }
96 |
97 | export const ResponsiveLayout = () => {
98 | return (
99 |
109 | {[...Array(24)].map((_, i) => (
110 |
111 |
112 |
113 | ))}
114 |
115 | )
116 | }
117 |
118 | const StyledBtn = styled.div`
119 | position: absolute;
120 | display: inline-flex;
121 | align-items: center;
122 | justify-content: center;
123 | height: 40px;
124 | width: 40px;
125 | font-size: 20px;
126 | color: red;
127 | opacity: 0.6;
128 | cursor: pointer;
129 | top: 50%;
130 | z-index: 10;
131 | transition: all 0.25s;
132 | transform: ${({ type }) =>
133 | `translateY(-50%) ${type === 'left' ? 'rotate(180deg)' : ''}`};
134 | left: ${({ type }) => (type === 'left' ? '20px' : 'initial')};
135 | right: ${({ type }) => (type === 'right' ? '20px' : 'initial')};
136 |
137 | &:hover {
138 | background: red;
139 | color: #fff;
140 | opacity: 0.5;
141 | }
142 | `
143 |
144 | const CustomDot = styled.span`
145 | display: inline-block;
146 | height: ${({ isActive }) => (isActive ? '8px' : '5px')};
147 | width: ${({ isActive }) => (isActive ? '8px' : '5px')};
148 | background: ${({ isActive }) => (isActive ? '#1890ff' : '#1890ff78')};
149 | transition: all 0.2s;
150 | `
151 |
152 | export const CustomArrowAndDot = () => {
153 | const LeftBtn = ➜
154 | const RightBtn = ➜
155 |
156 | return (
157 |
181 | {[...Array(15)].map((_, i) => (
182 |
183 |
184 |
185 | ))}
186 |
187 | )
188 | }
189 |
190 | const Card = styled.div`
191 | cursor: pointer;
192 | padding: 5px;
193 |
194 | &:hover {
195 | background: #f3f3f3;
196 | }
197 |
198 | img {
199 | width: 100%;
200 | border-radius: 8px;
201 | }
202 |
203 | div {
204 | margin-top: 5px;
205 | font-size: 12px;
206 | }
207 |
208 | span:first-of-type {
209 | color: red;
210 | font-weight: bold;
211 | font-size: 16px;
212 | }
213 |
214 | span:last-of-type {
215 | color: gray;
216 | margin-left: 5px;
217 | text-decoration-line: line-through;
218 | }
219 | `
220 |
221 | export const ProductCard = () => {
222 | return (
223 |
224 | {[...Array(15)].map((_, i) => (
225 |
226 |
227 |
228 |
229 | Apple AirPods with Wireless Charging Case (Latest Model)
230 |
231 | $5880
232 | $6990
233 |
234 |
235 |
236 | ))}
237 |
238 | )
239 | }
240 |
241 | const NewsItem = styled.div`
242 | cursor: pointer;
243 | position: relative;
244 |
245 | img {
246 | width: 100%;
247 | height: 150px;
248 | object-fit: cover;
249 | border-radius: 8px;
250 | }
251 |
252 | div {
253 | font-size: 12px;
254 | }
255 |
256 | @media screen and (max-width: 767px) {
257 | span {
258 | position: absolute;
259 | left: 0;
260 | bottom: 0;
261 | border-radius: 8px;
262 | padding: 110px 10px 10px 10px;
263 | display: inline-block;
264 | width: 100%;
265 | box-sizing: border-box;
266 | background: linear-gradient(0deg, #000, transparent);
267 | margin-top: 12px;
268 | color: #fff;
269 | font-weight: bold;
270 | font-size: 16px;
271 | }
272 | }
273 | `
274 |
275 | const ReadMore = styled.div`
276 | cursor: pointer;
277 | border-radius: 8px;
278 | background: gray;
279 | color: #fff;
280 | align-items: center;
281 | justify-content: center;
282 | display: flex;
283 | height: 100%;
284 | `
285 |
286 | export const NewsCarousel = () => {
287 | return (
288 |
289 | {[...Array(11), 'READ_MORE'].map((val, i) => {
290 | return (
291 |
292 | {val === 'READ_MORE' ? (
293 | Read More
294 | ) : (
295 |
296 |
297 |
298 | {`Trump downplays soldiers' head injuries in Iraq attacks`}
299 |
300 |
301 | )}
302 |
303 | )
304 | })}
305 |
306 | )
307 | }
308 |
309 | export const CustomProps = () => {
310 | const cols = number('cols', 1, {
311 | min: 1,
312 | max: 10,
313 | step: 1,
314 | range: true
315 | })
316 |
317 | const rows = number('rows', 1, {
318 | min: 1,
319 | max: 5,
320 | step: 1,
321 | range: true
322 | })
323 |
324 | const gap = number('gap (px)', 10, {
325 | min: 0,
326 | max: 20,
327 | step: 1,
328 | range: true
329 | })
330 |
331 | const pages = number('pages', 2, {
332 | min: 1,
333 | max: 5,
334 | step: 1,
335 | range: true
336 | })
337 |
338 | const loop = boolean('loop', false)
339 |
340 | const scrollSnap = boolean('scrollSnap', true)
341 |
342 | return (
343 |
350 | {[...Array(cols * rows * pages)].map((_, i) => (
351 |
352 |
353 |
354 | ))}
355 |
356 | )
357 | }
358 |
359 | export default {
360 | title: 'Carousel',
361 | component: Carousel,
362 | decorators: [
363 | withKnobs,
364 | story => (
365 |
374 | )
375 | ]
376 | }
377 |
--------------------------------------------------------------------------------