├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── .yarn
├── install-state.gz
└── releases
│ └── yarn-4.0.2.cjs
├── .yarnrc.yml
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── components
├── Layout.tsx
├── RenderComponent.tsx
└── library
│ ├── Accordion
│ ├── Preview
│ │ ├── Accordion.jsx
│ │ ├── Example.jsx
│ │ └── index.ts
│ ├── index.ts
│ └── tabs.ts
│ ├── ActionCard
│ ├── Preview
│ │ ├── ActionCard.jsx
│ │ ├── Example.jsx
│ │ └── index.ts
│ ├── index.ts
│ └── tabs.ts
│ ├── DateRangePicker
│ ├── Preview
│ │ ├── DateRangePicker.jsx
│ │ ├── Example.jsx
│ │ └── index.ts
│ ├── index.ts
│ └── tabs.ts
│ ├── FeedbackCard
│ ├── Preview
│ │ ├── Example.jsx
│ │ ├── FeedbackCard.jsx
│ │ └── index.ts
│ ├── index.ts
│ └── tabs.ts
│ ├── Knob
│ ├── Preview
│ │ ├── Example.jsx
│ │ ├── Knob.jsx
│ │ ├── Knob.module.css
│ │ └── index.ts
│ ├── index.ts
│ └── tabs.ts
│ ├── MediaGrid
│ ├── Preview
│ │ ├── Example.jsx
│ │ ├── MediaGrid.jsx
│ │ ├── MediaGrid.module.css
│ │ └── index.ts
│ ├── index.ts
│ └── tabs.ts
│ ├── NavCard
│ ├── Preview
│ │ ├── Example.jsx
│ │ ├── NavCard.jsx
│ │ └── index.ts
│ ├── index.ts
│ └── tabs.ts
│ ├── PricingCard
│ ├── Preview
│ │ ├── Example.jsx
│ │ ├── PricingCard.jsx
│ │ └── index.ts
│ ├── index.ts
│ └── tabs.ts
│ ├── ReviewBanner
│ ├── Preview
│ │ ├── Example.jsx
│ │ ├── ReviewBanner.jsx
│ │ └── index.ts
│ ├── index.ts
│ └── tabs.ts
│ ├── RichTextEditor
│ ├── Preview
│ │ ├── Example.jsx
│ │ ├── RichTextEditor.jsx
│ │ └── index.ts
│ ├── index.ts
│ └── tabs.ts
│ ├── SetupGuide
│ ├── Preview
│ │ ├── Example.jsx
│ │ ├── SetupGuide.jsx
│ │ ├── SetupGuide.module.css
│ │ └── index.ts
│ ├── index.ts
│ └── tabs.ts
│ ├── SortableList
│ ├── Preview
│ │ ├── Example.jsx
│ │ ├── SortableList.jsx
│ │ ├── SortableList.module.css
│ │ └── index.ts
│ ├── index.ts
│ └── tabs.ts
│ ├── StatBox
│ ├── Preview
│ │ ├── Example.jsx
│ │ ├── StatBox.jsx
│ │ └── index.ts
│ ├── index.ts
│ └── tabs.ts
│ └── Timeline
│ ├── Preview
│ ├── Example.jsx
│ ├── Timeline.jsx
│ ├── index.ts
│ └── timeline.module.css
│ ├── index.ts
│ └── tabs.ts
├── next.config.js
├── package.json
├── pages
├── _app.tsx
├── _document.tsx
├── components
│ └── [component].tsx
└── index.tsx
├── postcss.config.js
├── public
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── logo.png
├── timeline-icon_loyalty.png
├── timeline-icon_ricemill.png
└── timeline-icon_security.png
├── styles
├── globals.css
└── rich-text-editor.css
├── tailwind.config.ts
├── tsconfig.json
├── types.ts
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
37 | # local archive
38 | /_archive
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "singleQuote": true,
4 | "jsxSingleQuote": true,
5 | "trailingComma": "none",
6 | "arrowParens": "always",
7 | "bracketSpacing": true,
8 | "printWidth": 100
9 | }
10 |
--------------------------------------------------------------------------------
/.yarn/install-state.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RAAbbott/polaris-components/0b36e71dc4c9ea8efb23f05ea920dd0e5e5a1121/.yarn/install-state.gz
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 | yarnPath: .yarn/releases/yarn-4.0.2.cjs
3 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | *(This is a work in progress)*
4 |
5 | **IMPORTANT - Before opening a pull request to contribute, make sure your component meets these guidelines:**
6 | - Compatible with the latest major version of [Polaris](https://polaris.shopify.com/) (currently v12) and the latest major version of [Polaris Icons](https://polaris.shopify.com/icons) (currently v8)
7 | - Follows design guidelines laid out by the Polaris docs ([design](https://polaris.shopify.com/design), [content](https://polaris.shopify.com/content), and [patterns](https://polaris.shopify.com/patterns))
8 | - Primarily built with components from the Polaris library, filling in blanks with html + css where needed (prefer inline styles for the sake of copy/paste ease, use a css module if needed)
9 | - External dependencies should be used *sparingly*
10 | - If adding a variation of an existing Polaris Component on the site, it should differ significantly in either function or appearance
11 |
12 | ---
13 |
14 | If you'd like to start getting familiar with the code and adding your own components while this is being worked on, fork this repo then clone to your computer to get started.
15 |
16 | After you've cloned the repo run `yarn install` then `yarn dev` to start the dev server. (Make sure you have `yarn` installed)
17 |
18 | The components are currently found under `/components/library` and follow this structure (**This structure isn't optimal and will be changing soon, but will still be similar**):
19 |
20 | ```
21 | /components
22 | /library
23 | /ActionCard
24 | /Preview
25 | - ActionCard.jsx
26 | - Example.jsx
27 | - index.ts
28 | - tabs.ts
29 | - index.ts
30 | /SetupGuide
31 | ...
32 | ```
33 |
34 |
35 | Each component folder has a few important parts:
36 |
37 | ## Preview
38 | The `/Preview` folder contains the code files needed to render the preview (top) section of the component page. In the case of the `ActionCard`, we have the `ActionCard.jsx` file which is the actual component code, `Example.jsx` which is the code that we export to render the component preview page, and `index.ts` which just exports the `Example` component as `Preview`.
39 |
40 | With this setup, the `Example.jsx` file is essentially what is rendered in the preview
41 |
42 | ## Tabs
43 | The `tabs.ts` file looks like this:
44 |
45 | ```
46 | const Example = `...Example.jsx code as string`
47 | const ActionCard = `...Action.jsx code as string`
48 |
49 | export const tabs = [
50 | { title: 'Example Usage', content: Example },
51 | { title: 'ActionCard.jsx', content: ActionCard }
52 | ];
53 | ```
54 |
55 | This is used to generate the tabs in the code editor section of the component page.
56 |
57 | You can also specify a `lang` attribute for each tab for files other than `jsx` (e.g. `css`, `tsx`...`).
58 |
59 | I originally did this method with the stringified jsx because of simplicity and flexibility, but plan to update it soon to just read the code directly from the component files.
60 |
61 | ## Index
62 | The `index.ts` file of the component folder (`/ActionCard/index.ts`) exports all relevant info to be rendered by the page.
63 |
64 | Required exports are a `tabs` array, a `Preview` component, and a `title` which is the page title. Some components will also have a `Banner` comopnent exported here (from a `Banner.tsx` file in this same folder) that is used to provide context for the component but should be used sparingly, mostly just for indicating the use of external dependencies.
65 |
66 | You should also export a `contributor` string which is your GitHub username, but it's not required. Your file should look like this:
67 |
68 | ```
69 | export { tabs } from './tabs'; // REQUIRED
70 | export { Preview } from './Preview'; // REQUIRED
71 | export const title = 'Action Card'; // REQUIRED
72 | export const contributor = 'RAAbbott';
73 | ```
74 |
75 | The last step is adding your component to the `Navigation` component in `Layout.tsx`. In the `items` prop of the component section, add a new item object with this structure:
76 |
77 | ```
78 | {
79 | label: 'Component Name',
80 | icon: AppsIcon,
81 | selected: asPath === '/components/component-name', // component name should be in kebab-case
82 | onClick: () => changePage('/components/component-name') // should use same route as above prop
83 | }
84 | ```
85 |
86 | ## Testing
87 |
88 | After you add these changes, verify that the component is showing up in the navigation bar and that clicking it takes you to the component page. Verify that the code files and the rendered preview are correct. We don't have any automated testing at the moment, but you should test your implementation from the perspective of a user, copying over the code files from the UI to a project with Polaris (v12) installed and ensuring it works as expected in a different setup/environment.
89 |
90 | If you build your component with this same structure in place, everything should show up correctly in the component preview and code files. If you have any questions you can reach out to me on [twitter](https://x.com/devwithalex) or open a discussion if you run into any bugs/roadblocks.
91 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 RAAbbott
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Polaris Components
2 |
3 | A collection of components for Shopify app developers, based on the Polaris UI library & design system
4 |
5 | ## Documentation
6 |
7 | Components are stored under `/components/library`
8 |
9 | ```
10 | components
11 | └── library
12 | └── ActionCard
13 | ├── Preview
14 | │ ├── ActionCard.jsx
15 | │ ├── Example.jsx
16 | │ └── index.ts
17 | ├── tabs.ts
18 | └── index.ts
19 | ...
20 | ```
21 |
22 | ## Contributing
23 |
24 | Please read the [contributing guide](/CONTRIBUTING.md).
25 |
26 | ## License
27 |
28 | Licensed under the [MIT license](https://github.com/RAAbbott/polaris-components/blob/main/LICENSE.md).
29 |
--------------------------------------------------------------------------------
/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren, useCallback, useState } from 'react';
2 | import { useRouter } from 'next/router';
3 | import { Badge, Frame, Navigation, TopBar, useBreakpoints } from '@shopify/polaris';
4 | import { AppsIcon, ChevronRightIcon, HomeIcon, ChatIcon, CodeIcon } from '@shopify/polaris-icons';
5 | import '@shopify/polaris/build/esm/styles.css';
6 | import { Analytics } from '@vercel/analytics/react';
7 |
8 | export const Layout = ({ children }: PropsWithChildren) => {
9 | const [showMobileNav, setShowMobileNav] = useState(false);
10 | const { asPath, pathname, push } = useRouter();
11 | const { mdDown } = useBreakpoints();
12 |
13 | const handleNavigationToggle = useCallback(() => {
14 | setShowMobileNav((prev) => !prev);
15 | }, []);
16 |
17 | const logo = {
18 | width: 250,
19 | topBarSource: '/logo.png',
20 | accessibilityLabel: 'Polaris Components logo'
21 | };
22 |
23 | const userMenu = (
24 | <>
25 |
26 | null}
31 | open={false}
32 | />
33 | >
34 | );
35 |
36 | const topBar = (
37 |
38 | );
39 |
40 | const changePage = async (route: string) => {
41 | await push(route, undefined, { shallow: true });
42 | if (mdDown) {
43 | handleNavigationToggle();
44 | }
45 | };
46 |
47 | const AppNavigation = (
48 |
49 |
50 | changePage('/')
57 | }
58 | ]}
59 | />
60 | changePage('/components/setup-guide')
68 | },
69 | {
70 | label: 'Pricing Card',
71 | icon: AppsIcon,
72 | selected: asPath === '/components/pricing-card',
73 | onClick: () => changePage('/components/pricing-card')
74 | },
75 | {
76 | label: 'Sortable List',
77 | icon: AppsIcon,
78 | selected: asPath === '/components/sortable-list',
79 | onClick: () => changePage('/components/sortable-list')
80 | },
81 | {
82 | label: 'Action Card',
83 | icon: AppsIcon,
84 | selected: asPath === '/components/action-card',
85 | onClick: () => changePage('/components/action-card')
86 | },
87 | {
88 | label: 'Nav Card',
89 | icon: AppsIcon,
90 | selected: asPath === '/components/nav-card',
91 | onClick: () => changePage('/components/nav-card')
92 | },
93 | {
94 | label: 'Feedback Card',
95 | icon: AppsIcon,
96 | selected: asPath === '/components/feedback-card',
97 | onClick: () => changePage('/components/feedback-card')
98 | },
99 | {
100 | label: 'Accordion',
101 | icon: AppsIcon,
102 | selected: asPath === '/components/accordion',
103 | onClick: () => changePage('/components/accordion')
104 | },
105 | {
106 | label: 'Date Range Picker',
107 | icon: AppsIcon,
108 | selected: asPath === '/components/date-range-picker',
109 | onClick: () => changePage('/components/date-range-picker')
110 | },
111 | {
112 | label: 'Media Grid',
113 | icon: AppsIcon,
114 | selected: asPath === '/components/media-grid',
115 | onClick: () => changePage('/components/media-grid')
116 | },
117 | {
118 | label: 'Stat Box',
119 | icon: AppsIcon,
120 | selected: asPath === '/components/stat-box',
121 | onClick: () => changePage('/components/stat-box')
122 | },
123 | {
124 | label: 'Rich Text Editor',
125 | icon: AppsIcon,
126 | selected: asPath === '/components/rich-text-editor',
127 | onClick: () => changePage('/components/rich-text-editor')
128 | },
129 | {
130 | label: 'Timeline',
131 | badge: New ,
132 | icon: AppsIcon,
133 | selected: asPath === '/components/timeline',
134 | onClick: () => changePage('/components/timeline')
135 | },
136 | {
137 | label: 'Knob',
138 | badge: New ,
139 | icon: AppsIcon,
140 | selected: asPath === '/components/knob',
141 | onClick: () => changePage('/components/knob')
142 | },
143 | {
144 | label: 'Review Banner',
145 | icon: AppsIcon,
146 | badge: New ,
147 | selected: asPath === '/components/review-banner',
148 | onClick: () => changePage('/components/review-banner')
149 | }
150 | ]}
151 | action={{
152 | icon: ChevronRightIcon,
153 | accessibilityLabel: 'Add',
154 | onClick: () => changePage('/components/setup-guide')
155 | }}
156 | />
157 |
158 |
159 | );
160 | return (
161 | <>
162 |
163 |
170 | {children}
171 |
172 |
173 | {/* Bottom nav bar items */}
174 |
179 |
195 |
196 | >
197 | );
198 | };
199 |
--------------------------------------------------------------------------------
/components/RenderComponent.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef, useCallback, MouseEvent, TouchEvent, Fragment } from 'react';
2 | import {
3 | Page,
4 | Layout,
5 | Toast,
6 | Button,
7 | InlineStack,
8 | Text,
9 | Modal,
10 | Banner,
11 | Box,
12 | Link
13 | } from '@shopify/polaris';
14 | import {
15 | ChevronDownIcon,
16 | ChevronUpIcon,
17 | DragHandleIcon,
18 | DuplicateIcon
19 | } from '@shopify/polaris-icons';
20 | import { LiveEditor, LiveProvider } from 'react-live';
21 | import { Contributor, PageComponent, Platform, UserEventType } from '@/types';
22 | import { track } from '@vercel/analytics';
23 |
24 | const getContributorLink = ({ username, platform }: Contributor) => {
25 | switch (platform) {
26 | case Platform.TWITTER:
27 | return `https://www.x.com/${username}`;
28 | case Platform.GITHUB:
29 | return `https://www.github.com/${username}`;
30 | }
31 | };
32 |
33 | export const RenderComponent = ({
34 | title,
35 | Preview,
36 | tabs,
37 | subtitle,
38 | dependencies,
39 | contributors
40 | }: PageComponent) => {
41 | const [tab, setTab] = useState(0);
42 | const [maxHeight, setMaxHeight] = useState(0);
43 | const [height, setHeight] = useState(250);
44 | const [prevHeight, setPrevHeight] = useState(250);
45 | const [isMinimized, setIsMinimized] = useState(false);
46 | const [modalOpen, setModalOpen] = useState(false);
47 | const ref = useRef(null);
48 |
49 | // State + markup for toast component
50 | const [active, setActive] = useState(false);
51 | const toggleActive = useCallback(() => setActive((active) => !active), []);
52 | const toastMarkup = active ? (
53 |
54 | ) : null;
55 |
56 | const updateMargin = () => {
57 | const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
58 | if (ref.current) {
59 | ref.current.style.marginRight = `-${scrollbarWidth}px`;
60 | }
61 | };
62 |
63 | useEffect(() => {
64 | setTab(0);
65 | updateMargin();
66 | }, [title]);
67 |
68 | useEffect(() => {
69 | const updateMaxHeight = () => {
70 | updateMargin();
71 | setMaxHeight(window.innerHeight * 0.8);
72 | };
73 |
74 | // Add resize event listener to update max height when window is resized
75 | window.addEventListener('resize', updateMaxHeight);
76 | updateMaxHeight();
77 |
78 | // Clean up the event listener
79 | return () => {
80 | window.removeEventListener('resize', updateMaxHeight);
81 | };
82 | }, []);
83 |
84 | const toggleMinimize = () => {
85 | if (isMinimized) {
86 | setHeight(Math.max(prevHeight, 150)); // restore to previous height or 150px, whichever is greater
87 | } else {
88 | setPrevHeight(height); // store current height
89 | setHeight(0); // set height to 0 for minimized toolbar
90 | }
91 | setIsMinimized(!isMinimized);
92 | };
93 |
94 | const handleEventListeners = (type: UserEventType, startY: number) => {
95 | const isTouch = type === UserEventType.TOUCH;
96 | const startType = isTouch ? 'touchmove' : 'mousemove'; // event type for resizeStartListener
97 | const endType = isTouch ? 'touchend' : 'mouseup'; // event type for resizeEndListener
98 |
99 | const handleResize = (newHeight: number) => {
100 | if (newHeight <= maxHeight && newHeight >= 0) {
101 | setHeight(newHeight);
102 | // Update minimized state based on the new height
103 | if (newHeight > 0) {
104 | setIsMinimized(false);
105 | } else if (newHeight <= 0) {
106 | setIsMinimized(true);
107 | setHeight(0); // Set to minimum visible height (50px)
108 | }
109 | }
110 | };
111 |
112 | const resizeStartListener = (e: TouchEvent | MouseEvent) => {
113 | let newHeight: number;
114 | if (isTouch) {
115 | newHeight = height + (startY - (e as TouchEvent).touches[0].clientY);
116 | } else {
117 | newHeight = height + (startY - (e as MouseEvent).clientY);
118 | }
119 |
120 | handleResize(newHeight);
121 | };
122 |
123 | const resizeEndListener = () => {
124 | // @ts-ignore
125 | window.removeEventListener(startType, resizeStartListener);
126 | window.removeEventListener(endType, resizeEndListener);
127 | };
128 |
129 | // @ts-ignore
130 | window.addEventListener(startType, resizeStartListener);
131 | window.addEventListener(endType, resizeEndListener);
132 | };
133 |
134 | const resizingTouch = (mouseDownEvent: TouchEvent) => {
135 | const startY = mouseDownEvent.touches[0].clientY;
136 | handleEventListeners(UserEventType.TOUCH, startY);
137 | };
138 |
139 | const resizingMouse = (mouseDownEvent: MouseEvent) => {
140 | const startY = mouseDownEvent.clientY;
141 | handleEventListeners(UserEventType.MOUSE, startY);
142 | };
143 |
144 | return (
145 |
154 |
155 | Contributed by{' '}
156 | {contributors.map((contributor, idx) => (
157 |
158 | {idx > 0 && ', '}
159 |
164 | {contributor.username}
165 |
166 |
167 | ))}
168 |
169 |
170 | ) : null
171 | }
172 | >
173 |
174 |
175 | {dependencies ? setModalOpen(true)} /> : null}
176 |
179 |
180 |
181 |
182 |
183 | {/* Read-only code editor */}
184 |
188 |
189 | {/* Background for toolbar */}
190 |
191 |
192 | {/* Minimize/maximize button */}
193 |
194 |
195 | {isMinimized ? (
196 |
197 | ) : (
198 |
199 | )}
200 |
201 |
202 |
203 | {/* Resize drag handler */}
204 |
213 |
214 | {/* Tabs for component files */}
215 |
216 |
217 | {tabs.map(({ title }: { title: string }, i: number) => (
218 |
setTab(i)}
222 | >
223 | {title}
224 |
225 | ))}
226 |
227 |
228 |
229 | {/* Code editor wrapper */}
230 |
237 | {/* Copy icon in top right */}
238 |
239 | {
243 | navigator.clipboard.writeText(tabs[tab].content);
244 | toggleActive();
245 | track('Copy File', { component: title, file: tabs[tab]?.title });
246 | }}
247 | />
248 |
249 |
250 | {/* Code editor */}
251 |
256 |
265 |
266 |
267 |
268 |
269 |
270 | {/* Toast */}
271 | {toastMarkup}
272 |
273 | {/* Dependency Modal */}
274 | {dependencies && dependencies.length ? (
275 | setModalOpen(false)}
279 | primaryAction={{
280 | content: 'Copy NPM command',
281 | onAction: () => {
282 | navigator.clipboard.writeText(`npm i ${dependencies.join(' ')}`);
283 | toggleActive();
284 | }
285 | }}
286 | >
287 |
288 | This component requires the following packages:
289 |
290 | {dependencies.map((dep) => {
291 | return (
292 |
293 | {dep}
294 |
295 | );
296 | })}
297 |
298 |
299 |
300 | ) : null}
301 |
302 | );
303 | };
304 |
305 | const DependencyBanner = ({ openModal }: { openModal: () => void }) => {
306 | return (
307 |
308 |
309 |
310 | Requires external dependencies
311 |
312 | Learn more
313 |
314 |
315 | );
316 | };
317 |
--------------------------------------------------------------------------------
/components/library/Accordion/Preview/Accordion.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Card, Box, InlineStack, Text, Collapsible } from '@shopify/polaris';
3 | import { ChevronUpIcon, ChevronDownIcon } from "@shopify/polaris-icons";
4 |
5 | export const Accordion = () => {
6 | const [expanded, setExpanded] = useState(0); // Set to null if none should be expanded by default
7 |
8 | return (
9 |
10 | {ACCORDION_ITEMS.map(({ title, id, content }) => {
11 | const isExpanded = expanded === id;
12 | return (
13 |
19 |
20 | {
23 | return setExpanded((prev) => (id === prev ? null : id));
24 | }}
25 | >
26 |
27 |
28 | {title}
29 |
30 | {isExpanded ? (
31 |
32 | ) : (
33 |
34 | )}
35 |
36 |
37 |
38 |
39 |
40 | {content}
41 |
42 |
43 |
44 | );
45 | })}
46 |
47 | );
48 | };
49 |
50 | const ACCORDION_ITEMS = [
51 | {
52 | id: 0,
53 | title: 'Step 1',
54 | content: (
55 |
56 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. Sequi tempore, saepe minima
57 | nesciunt impedit quaerat repellat eveniet, dignissimos quis quo sed maxime aspernatur qui,
58 | quod consectetur optio veritatis iusto eligendi?
59 |
60 | )
61 | },
62 | {
63 | id: 1,
64 | title: 'Step 2',
65 | content: (
66 |
67 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. Sequi tempore, saepe minima
68 | nesciunt impedit quaerat repellat eveniet, dignissimos quis quo sed maxime aspernatur qui,
69 | quod consectetur optio veritatis iusto eligendi?
70 |
71 | )
72 | },
73 | {
74 | id: 2,
75 | title: 'Step 3',
76 | content: (
77 |
78 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. Sequi tempore, saepe minima
79 | nesciunt impedit quaerat repellat eveniet, dignissimos quis quo sed maxime aspernatur qui,
80 | quod consectetur optio veritatis iusto eligendi?
81 |
82 | )
83 | },
84 | {
85 | id: 3,
86 | title: 'Step 4',
87 | content: (
88 |
89 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. Sequi tempore, saepe minima
90 | nesciunt impedit quaerat repellat eveniet, dignissimos quis quo sed maxime aspernatur qui,
91 | quod consectetur optio veritatis iusto eligendi?
92 |
93 | )
94 | }
95 | ];
96 |
--------------------------------------------------------------------------------
/components/library/Accordion/Preview/Example.jsx:
--------------------------------------------------------------------------------
1 | import { Layout, Page } from '@shopify/polaris';
2 | import { Accordion } from './Accordion';
3 |
4 | export const Example = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/components/library/Accordion/Preview/index.ts:
--------------------------------------------------------------------------------
1 | export { Example as Preview } from "./Example";
2 |
--------------------------------------------------------------------------------
/components/library/Accordion/index.ts:
--------------------------------------------------------------------------------
1 | // Index files for each component directory should export the tabs, Component, and title
2 |
3 | import { Platform } from '@/types';
4 |
5 | export { tabs } from './tabs';
6 | export { Preview } from './Preview';
7 | export const title = 'Accordion';
8 | export const contributors = [{ username: 'devwithalex', platform: Platform.TWITTER }];
9 | export const subtitle = 'Usage: FAQs, Wizards';
10 |
--------------------------------------------------------------------------------
/components/library/Accordion/tabs.ts:
--------------------------------------------------------------------------------
1 | import { Tab } from '@/types';
2 |
3 | const Accordion = `import { useState } from 'react';
4 | import { Card, Box, InlineStack, Text, Collapsible } from '@shopify/polaris';
5 | import { ChevronUpIcon, ChevronDownIcon } from '@shopify/polaris-icons';
6 |
7 | export const Accordion = () => {
8 | const [expanded, setExpanded] = useState(0); // Set to null if none should be expanded by default
9 |
10 | return (
11 |
12 | {ACCORDION_ITEMS.map(({ title, id, content }) => {
13 | const isExpanded = expanded === id;
14 | return (
15 |
21 |
22 | {
25 | return setExpanded((prev) => (id === prev ? null : id));
26 | }}
27 | >
28 |
29 |
30 | {title}
31 |
32 | {isExpanded ? (
33 |
34 | ) : (
35 |
36 | )}
37 |
38 |
39 |
40 |
41 |
42 | {content}
43 |
44 |
45 |
46 | );
47 | })}
48 |
49 | );
50 | };
51 |
52 | const ACCORDION_ITEMS = [
53 | {
54 | id: 0,
55 | title: 'Step 1',
56 | content: (
57 |
58 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. Sequi tempore, saepe minima
59 | nesciunt impedit quaerat repellat eveniet, dignissimos quis quo sed maxime aspernatur qui,
60 | quod consectetur optio veritatis iusto eligendi?
61 |
62 | )
63 | },
64 | {
65 | id: 1,
66 | title: 'Step 2',
67 | content: (
68 |
69 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. Sequi tempore, saepe minima
70 | nesciunt impedit quaerat repellat eveniet, dignissimos quis quo sed maxime aspernatur qui,
71 | quod consectetur optio veritatis iusto eligendi?
72 |
73 | )
74 | },
75 | {
76 | id: 2,
77 | title: 'Step 3',
78 | content: (
79 |
80 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. Sequi tempore, saepe minima
81 | nesciunt impedit quaerat repellat eveniet, dignissimos quis quo sed maxime aspernatur qui,
82 | quod consectetur optio veritatis iusto eligendi?
83 |
84 | )
85 | },
86 | {
87 | id: 3,
88 | title: 'Step 4',
89 | content: (
90 |
91 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. Sequi tempore, saepe minima
92 | nesciunt impedit quaerat repellat eveniet, dignissimos quis quo sed maxime aspernatur qui,
93 | quod consectetur optio veritatis iusto eligendi?
94 |
95 | )
96 | }
97 | ];
98 | `;
99 |
100 | const Example = `import { Layout, Page } from '@shopify/polaris';
101 | import { Accordion } from './Accordion';
102 |
103 | export const Example = () => {
104 | return (
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | );
113 | };
114 | `;
115 |
116 | export const tabs: Tab[] = [
117 | { title: 'Example Usage', content: Example },
118 | { title: 'Accordion.jsx', content: Accordion }
119 | ];
120 |
--------------------------------------------------------------------------------
/components/library/ActionCard/Preview/ActionCard.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Card, BlockStack, Text, Button, ActionList, Popover } from "@shopify/polaris";
3 |
4 | export const ActionCard = () => {
5 | const [active, setActive] = useState(false);
6 |
7 | return (
8 |
9 |
10 |
11 |
12 | Payment Methods
13 |
14 |
15 | Payments that are made outside your online store. When a customer selects a manual payment method such as
16 | cash on delivery, you'll need to approve their order before it can be fulfilled.
17 |
18 |
19 | setActive((prev) => !prev)} disclosure>
23 | Add manual payment method
24 |
25 | }
26 | autofocusTarget="first-node"
27 | onClose={() => setActive((prev) => !prev)}
28 | >
29 | null,
35 | },
36 | {
37 | content: "Bank Deposit",
38 | onAction: () => null,
39 | },
40 | ]}
41 | />
42 |
43 |
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/components/library/ActionCard/Preview/Example.jsx:
--------------------------------------------------------------------------------
1 | import { Layout, Page } from "@shopify/polaris";
2 | import { ActionCard } from "./ActionCard";
3 |
4 | export const Example = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/components/library/ActionCard/Preview/index.ts:
--------------------------------------------------------------------------------
1 | export { Example as Preview } from "./Example";
2 |
--------------------------------------------------------------------------------
/components/library/ActionCard/index.ts:
--------------------------------------------------------------------------------
1 | // Index files for each component directory should export the tabs, Component, and title
2 |
3 | import { Platform } from '@/types';
4 |
5 | export { tabs } from './tabs';
6 | export { Preview } from './Preview';
7 | export const title = 'Action Card';
8 | export const contributors = [{ username: 'devwithalex', platform: Platform.TWITTER }];
9 |
--------------------------------------------------------------------------------
/components/library/ActionCard/tabs.ts:
--------------------------------------------------------------------------------
1 | import { Tab } from '@/types';
2 |
3 | const ActionCard = `import { useState } from "react";
4 | import { Card, BlockStack, Text, Button, ActionList, Popover } from "@shopify/polaris";
5 |
6 | export const ActionCard = () => {
7 | const [active, setActive] = useState(false);
8 |
9 | return (
10 |
11 |
12 |
13 |
14 | Payment Methods
15 |
16 |
17 | Payments that are made outside your online store. When a customer selects a manual payment method such as
18 | cash on delivery, you'll need to approve their order before it can be fulfilled.
19 |
20 |
21 | setActive((prev) => !prev)} disclosure>
25 | Add manual payment method
26 |
27 | }
28 | autofocusTarget="first-node"
29 | onClose={() => setActive((prev) => !prev)}
30 | >
31 | null,
37 | },
38 | {
39 | content: "Bank Deposit",
40 | onAction: () => null,
41 | },
42 | ]}
43 | />
44 |
45 |
46 |
47 | );
48 | };`;
49 |
50 | const Example = `import { Layout, Page } from "@shopify/polaris";
51 | import { ActionCard } from "./ActionCard";
52 |
53 | export const Example = () => {
54 | return (
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | };
64 | `;
65 |
66 | export const tabs: Tab[] = [
67 | { title: 'Example Usage', content: Example },
68 | { title: 'ActionCard.jsx', content: ActionCard }
69 | ];
70 |
--------------------------------------------------------------------------------
/components/library/DateRangePicker/Preview/DateRangePicker.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback, useEffect } from 'react';
2 | import {
3 | Popover,
4 | Button,
5 | TextField,
6 | Box,
7 | DatePicker,
8 | Icon,
9 | OptionList,
10 | Scrollable,
11 | InlineGrid,
12 | BlockStack,
13 | InlineStack,
14 | useBreakpoints
15 | } from '@shopify/polaris';
16 | import { ArrowRightIcon, CalendarIcon } from '@shopify/polaris-icons';
17 |
18 | export const DateRangePicker = ({ onDateRangeSelect, value: { start, end } }) => {
19 | const { mdDown } = useBreakpoints();
20 | const [popoverActive, setPopoverActive] = useState(false);
21 | const today = new Date(new Date().setHours(0, 0, 0, 0));
22 | const yesterday = new Date(
23 | new Date(new Date().setDate(today.getDate() - 1)).setHours(0, 0, 0, 0)
24 | );
25 | const ranges = [
26 | { title: 'Today', period: { since: today, until: today } },
27 | { title: 'Yesterday', period: { since: yesterday, until: yesterday } },
28 | {
29 | title: 'Last 7 days',
30 | period: {
31 | since: new Date(new Date().setDate(today.getDate() - 7)),
32 | until: yesterday
33 | }
34 | },
35 | {
36 | title: 'Last 30 days',
37 | period: {
38 | since: new Date(new Date().setDate(today.getDate() - 30)),
39 | until: yesterday
40 | }
41 | },
42 | {
43 | title: 'Last 90 days',
44 | period: {
45 | since: new Date(new Date().setDate(today.getDate() - 90)),
46 | until: yesterday
47 | }
48 | },
49 | {
50 | title: 'Last 365 Days',
51 | period: {
52 | since: new Date(new Date().setDate(today.getDate() - 365)),
53 | until: yesterday
54 | }
55 | },
56 | {
57 | title: 'Custom',
58 | period: { since: yesterday, until: yesterday }
59 | }
60 | ];
61 |
62 | const getDefaultDateRange = () => {
63 | const areDatesEqual = (dateX, dateY) => dateX.toDateString() == dateY.toDateString();
64 |
65 | if (start && end) {
66 | const currentRange = ranges.find((range) => {
67 | const { since, until } = range.period;
68 |
69 | return areDatesEqual(since, start) && areDatesEqual(until, end)
70 | });
71 |
72 | if (currentRange) {
73 | return currentRange;
74 | } else {
75 | return { title: 'Custom', period: { since: start, until: end } };
76 | }
77 | }
78 |
79 | return ranges[0];
80 | };
81 |
82 | const defaultRange = getDefaultDateRange();
83 | const [activeDateRange, setActiveDateRange] = useState(defaultRange);
84 | const [dateState, setDateState] = useState({
85 | month: activeDateRange.period.since.getMonth(),
86 | year: activeDateRange.period.since.getFullYear()
87 | });
88 |
89 | const handleMonthChange = useCallback((month, year) => {
90 | setDateState({ month, year });
91 | }, []);
92 |
93 | const formatDate = (date) => date.toISOString().split('T')[0];
94 |
95 | useEffect(() => {
96 | setDateState({
97 | month: activeDateRange.period.since.getMonth(),
98 | year: activeDateRange.period.since.getFullYear()
99 | });
100 | }, [activeDateRange]);
101 |
102 | return (
103 |
104 | setPopoverActive(!popoverActive)}>
114 | {activeDateRange.title}
115 |
116 | }
117 | onClose={() => setPopoverActive(false)}
118 | >
119 |
127 |
128 |
132 |
139 |
140 | ({
142 | value: range.title,
143 | label: (
144 |
149 | {range.title}
150 |
151 | )
152 | }))}
153 | selected={activeDateRange.title}
154 | onChange={(selected) => {
155 | const selectedRange = ranges.find((range) => range.title === selected[0]);
156 | setActiveDateRange(selectedRange);
157 | }}
158 | />
159 |
160 |
161 |
162 |
163 |
164 |
165 |
172 |
173 | {!mdDown ? (
174 |
179 |
180 |
181 | ) : null}
182 |
183 |
184 |
191 |
192 |
193 |
194 | {
202 | setActiveDateRange({
203 | title: 'Custom',
204 | period: { since: start, until: end }
205 | });
206 | }}
207 | onMonthChange={handleMonthChange}
208 | multiMonth={mdDown ? false : true}
209 | allowRange
210 | />
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 | {
221 | setActiveDateRange(defaultRange);
222 | setPopoverActive(false);
223 | }}
224 | >
225 | Cancel
226 |
227 | {
230 | onDateRangeSelect({
231 | start: activeDateRange.period.since,
232 | end: activeDateRange.period.until
233 | });
234 | setPopoverActive(false);
235 | }}
236 | >
237 | Apply
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 | );
246 | };
247 |
--------------------------------------------------------------------------------
/components/library/DateRangePicker/Preview/Example.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Layout, Page } from '@shopify/polaris';
3 | import { DateRangePicker } from './DateRangePicker';
4 |
5 | export const Example = () => {
6 | const [date, setDate] = useState({}); // {start, end}
7 |
8 | return (
9 |
10 |
11 |
12 | {
15 | console.log('Selected Start Date:', start);
16 | console.log('Selected End Date:', end);
17 | // You can now do whatever you need with these dates, like setting state or making API calls
18 | setDate({ start, end });
19 | }}
20 | />
21 |
22 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/components/library/DateRangePicker/Preview/index.ts:
--------------------------------------------------------------------------------
1 | export { Example as Preview } from './Example';
2 |
--------------------------------------------------------------------------------
/components/library/DateRangePicker/index.ts:
--------------------------------------------------------------------------------
1 | // Index files for each component directory should export the tabs, Component, and title
2 |
3 | import { Platform } from '@/types';
4 |
5 | export { tabs } from './tabs';
6 | export { Preview } from './Preview';
7 | export const title = 'Date Picker';
8 | export const contributors = [{ username: 'tkejr', platform: Platform.GITHUB }];
9 |
--------------------------------------------------------------------------------
/components/library/DateRangePicker/tabs.ts:
--------------------------------------------------------------------------------
1 | import { Tab } from '@/types';
2 |
3 | const DateRangePicker = `import React, { useState, useCallback, useEffect } from 'react';
4 | import {
5 | Popover,
6 | Button,
7 | TextField,
8 | Box,
9 | DatePicker,
10 | Icon,
11 | OptionList,
12 | Scrollable,
13 | InlineGrid,
14 | BlockStack,
15 | InlineStack,
16 | useBreakpoints
17 | } from '@shopify/polaris';
18 | import { ArrowRightIcon, CalendarIcon } from '@shopify/polaris-icons';
19 |
20 | export const DateRangePicker = ({ onDateRangeSelect, value: { start, end } }) => {
21 | const { mdDown } = useBreakpoints();
22 | const [popoverActive, setPopoverActive] = useState(false);
23 | const today = new Date(new Date().setHours(0, 0, 0, 0));
24 | const yesterday = new Date(
25 | new Date(new Date().setDate(today.getDate() - 1)).setHours(0, 0, 0, 0)
26 | );
27 | const ranges = [
28 | { title: 'Today', period: { since: today, until: today } },
29 | { title: 'Yesterday', period: { since: yesterday, until: yesterday } },
30 | {
31 | title: 'Last 7 days',
32 | period: {
33 | since: new Date(new Date().setDate(today.getDate() - 7)),
34 | until: yesterday
35 | }
36 | },
37 | {
38 | title: 'Last 30 days',
39 | period: {
40 | since: new Date(new Date().setDate(today.getDate() - 30)),
41 | until: yesterday
42 | }
43 | },
44 | {
45 | title: 'Last 90 days',
46 | period: {
47 | since: new Date(new Date().setDate(today.getDate() - 90)),
48 | until: yesterday
49 | }
50 | },
51 | {
52 | title: 'Last 365 Days',
53 | period: {
54 | since: new Date(new Date().setDate(today.getDate() - 365)),
55 | until: yesterday
56 | }
57 | },
58 | {
59 | title: 'Custom',
60 | period: { since: yesterday, until: yesterday }
61 | }
62 | ];
63 |
64 | const getDefaultDateRange = () => {
65 | const areDatesEqual = (dateX, dateY) => dateX.toDateString() == dateY.toDateString();
66 |
67 | if (start && end) {
68 | const currentRange = ranges.find((range) => {
69 | const { since, until } = range.period;
70 |
71 | return areDatesEqual(since, start) && areDatesEqual(until, end)
72 | });
73 |
74 | if (currentRange) {
75 | return currentRange;
76 | } else {
77 | return { title: 'Custom', period: { since: start, until: end } };
78 | }
79 | }
80 |
81 | return ranges[0];
82 | };
83 |
84 | const defaultRange = getDefaultDateRange();
85 | const [activeDateRange, setActiveDateRange] = useState(defaultRange);
86 | const [dateState, setDateState] = useState({
87 | month: activeDateRange.period.since.getMonth(),
88 | year: activeDateRange.period.since.getFullYear()
89 | });
90 |
91 | const handleMonthChange = useCallback((month, year) => {
92 | setDateState({ month, year });
93 | }, []);
94 |
95 | const formatDate = (date) => date.toISOString().split('T')[0];
96 |
97 | useEffect(() => {
98 | setDateState({
99 | month: activeDateRange.period.since.getMonth(),
100 | year: activeDateRange.period.since.getFullYear()
101 | });
102 | }, [activeDateRange]);
103 |
104 | return (
105 |
106 | setPopoverActive(!popoverActive)}>
116 | {activeDateRange.title}
117 |
118 | }
119 | onClose={() => setPopoverActive(false)}
120 | >
121 |
129 |
130 |
134 |
141 |
142 | ({
144 | value: range.title,
145 | label: (
146 |
151 | {range.title}
152 |
153 | )
154 | }))}
155 | selected={activeDateRange.title}
156 | onChange={(selected) => {
157 | const selectedRange = ranges.find((range) => range.title === selected[0]);
158 | setActiveDateRange(selectedRange);
159 | }}
160 | />
161 |
162 |
163 |
164 |
165 |
166 |
167 |
174 |
175 | {!mdDown ? (
176 |
181 |
182 |
183 | ) : null}
184 |
185 |
186 |
193 |
194 |
195 |
196 | {
204 | setActiveDateRange({
205 | title: 'Custom',
206 | period: { since: start, until: end }
207 | });
208 | }}
209 | onMonthChange={handleMonthChange}
210 | multiMonth={mdDown ? false : true}
211 | allowRange
212 | />
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 | {
223 | setActiveDateRange(defaultRange);
224 | setPopoverActive(false);
225 | }}
226 | >
227 | Cancel
228 |
229 | {
232 | onDateRangeSelect({
233 | start: activeDateRange.period.since,
234 | end: activeDateRange.period.until
235 | });
236 | setPopoverActive(false);
237 | }}
238 | >
239 | Apply
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 | );
248 | };
249 |
250 | `;
251 |
252 | const Example = `import { useState } from 'react';
253 | import { Layout, Page } from '@shopify/polaris';
254 | import { DateRangePicker } from './DateRangePicker';
255 |
256 | export const Example = () => {
257 | const [date, setDate] = useState({}); // {start, end}
258 |
259 | return (
260 |
261 |
262 |
263 | {
266 | console.log('Selected Start Date:', start);
267 | console.log('Selected End Date:', end);
268 | // You can now do whatever you need with these dates, like setting state or making API calls
269 | setDate({ start, end });
270 | }}
271 | />
272 |
273 |
274 |
275 | );
276 | };
277 |
278 | `;
279 |
280 | export const tabs: Tab[] = [
281 | { title: 'Example Usage', content: Example },
282 | { title: 'DateRangePicker.jsx', content: DateRangePicker }
283 | ];
284 |
--------------------------------------------------------------------------------
/components/library/FeedbackCard/Preview/Example.jsx:
--------------------------------------------------------------------------------
1 | import { Layout, Page } from '@shopify/polaris';
2 | import { FeedbackCard } from './FeedbackCard';
3 |
4 | export const Example = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/components/library/FeedbackCard/Preview/FeedbackCard.jsx:
--------------------------------------------------------------------------------
1 | import { Card, BlockStack, Text, Button, ButtonGroup, InlineStack } from '@shopify/polaris';
2 | import { XIcon, ThumbsDownIcon, ThumbsUpIcon } from "@shopify/polaris-icons";
3 |
4 | export const FeedbackCard = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 | Share your feedback
12 |
13 |
14 |
15 |
16 | How would you describe your experience using the Polaris Components app?
17 |
18 |
19 |
20 | null}>
21 | Good
22 |
23 | null}>
24 | Bad
25 |
26 |
27 |
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/components/library/FeedbackCard/Preview/index.ts:
--------------------------------------------------------------------------------
1 | export { Example as Preview } from "./Example";
2 |
--------------------------------------------------------------------------------
/components/library/FeedbackCard/index.ts:
--------------------------------------------------------------------------------
1 | // Index files for each component directory should export the tabs, Component, and title
2 |
3 | import { Platform } from '@/types';
4 |
5 | export { tabs } from './tabs';
6 | export { Preview } from './Preview';
7 | export const title = 'Feedback Card';
8 | export const contributors = [{ username: 'devwithalex', platform: Platform.TWITTER }];
9 |
--------------------------------------------------------------------------------
/components/library/FeedbackCard/tabs.ts:
--------------------------------------------------------------------------------
1 | import { Tab } from '@/types';
2 |
3 | const FeedbackCard = `import { Card, BlockStack, Text, Button, ButtonGroup, InlineStack } from '@shopify/polaris';
4 | import { XIcon, ThumbsDownIcon, ThumbsUpIcon } from "@shopify/polaris-icons";
5 |
6 | export const FeedbackCard = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | Share your feedback
14 |
15 |
16 |
17 |
18 | How would you describe your experience using the Polaris Components app?
19 |
20 |
21 |
22 | null}>
23 | Good
24 |
25 | null}>
26 | Bad
27 |
28 |
29 |
30 |
31 | );
32 | };`;
33 |
34 | const Example = `import { Layout, Page } from '@shopify/polaris';
35 | import { FeedbackCard } from './FeedbackCard';
36 |
37 | export const Example = () => {
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | };
48 | `;
49 |
50 | export const tabs: Tab[] = [
51 | { title: 'Example Usage', content: Example },
52 | { title: 'FeedbackCard.jsx', content: FeedbackCard }
53 | ];
54 |
--------------------------------------------------------------------------------
/components/library/Knob/Preview/Example.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Page, Layout, Card, InlineStack, Text, Badge } from '@shopify/polaris';
3 | import { Knob } from './Knob';
4 |
5 | export function Example() {
6 | const [selected, setSelected] = useState(false);
7 |
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Toggle Knob
17 |
18 |
19 | {selected ? 'Enabled' : 'Disabled'}
20 |
21 |
22 | setSelected((prev) => !prev)}
26 | />
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/components/library/Knob/Preview/Knob.jsx:
--------------------------------------------------------------------------------
1 | import styles from './Knob.module.css';
2 |
3 | /**
4 | * @typedef {Object} KnobProps
5 | * @property {string} ariaLabel - The aria-label for the knob
6 | * @property {boolean} selected - Whether the knob is selected or not
7 | * @property {() => void} onClick - The function to call when the knob is clicked
8 | */
9 |
10 | /**
11 | * Knob component
12 | * @param {KnobProps} props - The props for the Knob component
13 | * @returns {JSX.Element} The rendered Knob component
14 | */
15 | export const Knob = ({ ariaLabel, selected, onClick }) => {
16 | return (
17 |
26 |
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/components/library/Knob/Preview/Knob.module.css:
--------------------------------------------------------------------------------
1 | .track {
2 | height: var(--track-height);
3 | width: 2rem;
4 | background: var(--p-color-icon-secondary);
5 | padding: 0.25rem;
6 | box-shadow: inset 0 0.0625rem 0.25rem rgba(0, 0, 0, 0.05);
7 | border: none;
8 | border-radius: 0.375rem;
9 | cursor: pointer;
10 | transition: background var(--p-motion-duration-50) var(--p-motion-ease);
11 | }
12 |
13 | .track:after {
14 | content: '';
15 | position: absolute;
16 | z-index: 1;
17 | top: -0.0625rem;
18 | right: -0.0625rem;
19 | bottom: -0.0625rem;
20 | left: -0.0625rem;
21 | display: block;
22 | pointer-events: none;
23 | box-shadow: 0 0 0 -0.0625rem var(--p-color-border-focus);
24 | transition: box-shadow var(--p-motion-duration-100) var(--p-motion-ease);
25 | border-radius: calc(var(--p-border-radius-100) + 0.0625rem);
26 | }
27 |
28 | .track_on {
29 | background: var(--p-color-bg-inverse);
30 | }
31 |
32 | .knob {
33 | height: 0.75rem;
34 | width: 0.75rem;
35 | border-radius: 0.1875rem;
36 | background: var(--p-color-bg-surface);
37 | transition: transform var(--p-motion-duration-50) var(--p-motion-ease);
38 | }
39 |
40 | .knob_on {
41 | transform: translate(100%);
42 | }
43 |
44 | .track:hover {
45 | background: rgba(97, 97, 97, 1);
46 | }
47 |
48 | .track_on:hover {
49 | background: rgba(48, 48, 48, 1);
50 | }
51 |
--------------------------------------------------------------------------------
/components/library/Knob/Preview/index.ts:
--------------------------------------------------------------------------------
1 | export { Example as Preview } from "./Example";
2 |
--------------------------------------------------------------------------------
/components/library/Knob/index.ts:
--------------------------------------------------------------------------------
1 | import { Platform } from '@/types';
2 |
3 | // Index files for each component should export the tabs, preview, and title
4 | export { tabs } from './tabs';
5 | export { Preview } from './Preview/index';
6 | export const title = 'Knob';
7 | export const contributors = [
8 | { username: 'denniscessan', platform: Platform.TWITTER },
9 | { username: 'SammyIsseyegh', platform: Platform.TWITTER }
10 | ];
11 |
--------------------------------------------------------------------------------
/components/library/Knob/tabs.ts:
--------------------------------------------------------------------------------
1 | import { Tab } from '@/types';
2 |
3 | const Example = `import { useState } from 'react';
4 | import { Page, Layout, Card, InlineStack, Text, Badge } from '@shopify/polaris';
5 | import { Knob } from './Knob';
6 |
7 | export function Example() {
8 | const [selected, setSelected] = useState(false);
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Toggle Knob
19 |
20 |
21 | {selected ? 'Enabled' : 'Disabled'}
22 |
23 |
24 | setSelected((prev) => !prev)}
28 | />
29 |
30 |
31 |
32 |
33 |
34 | );
35 | }
36 | `;
37 |
38 | const Knob = `import styles from './Knob.module.css';
39 |
40 | /**
41 | * @typedef {Object} KnobProps
42 | * @property {string} ariaLabel - The aria-label for the knob
43 | * @property {boolean} selected - Whether the knob is selected or not
44 | * @property {() => void} onClick - The function to call when the knob is clicked
45 | */
46 |
47 | /**
48 | * Knob component
49 | * @param {KnobProps} props - The props for the Knob component
50 | * @returns {JSX.Element} The rendered Knob component
51 | */
52 | export const Knob = ({ ariaLabel, selected, onClick }) => {
53 | return (
54 |
63 |
64 |
65 | );
66 | };
67 | `;
68 |
69 | const CSS = `.track {
70 | height: var(--track-height);
71 | width: 2rem;
72 | background: var(--p-color-icon-secondary);
73 | padding: 0.25rem;
74 | box-shadow: inset 0 0.0625rem 0.25rem rgba(0, 0, 0, 0.05);
75 | border: none;
76 | border-radius: 0.375rem;
77 | cursor: pointer;
78 | transition: background var(--p-motion-duration-50) var(--p-motion-ease);
79 | }
80 |
81 | .track:after {
82 | content: '';
83 | position: absolute;
84 | z-index: 1;
85 | top: -0.0625rem;
86 | right: -0.0625rem;
87 | bottom: -0.0625rem;
88 | left: -0.0625rem;
89 | display: block;
90 | pointer-events: none;
91 | box-shadow: 0 0 0 -0.0625rem var(--p-color-border-focus);
92 | transition: box-shadow var(--p-motion-duration-100) var(--p-motion-ease);
93 | border-radius: calc(var(--p-border-radius-100) + 0.0625rem);
94 | }
95 |
96 | .track_on {
97 | background: var(--p-color-bg-inverse);
98 | }
99 |
100 | .knob {
101 | height: 0.75rem;
102 | width: 0.75rem;
103 | border-radius: 0.1875rem;
104 | background: var(--p-color-bg-surface);
105 | transition: transform var(--p-motion-duration-50) var(--p-motion-ease);
106 | }
107 |
108 | .knob_on {
109 | transform: translate(100%);
110 | }
111 |
112 | .track:hover {
113 | background: rgba(97, 97, 97, 1);
114 | }
115 |
116 | .track_on:hover {
117 | background: rgba(48, 48, 48, 1);
118 | }
119 | `;
120 |
121 | export const tabs: Tab[] = [
122 | { title: 'Example Usage', content: Example },
123 | { title: 'Knob.jsx', content: Knob },
124 | { title: 'Knob.module.css', content: CSS, lang: 'css' }
125 | ];
126 |
--------------------------------------------------------------------------------
/components/library/MediaGrid/Preview/Example.jsx:
--------------------------------------------------------------------------------
1 | import { Layout, Page, Card, BlockStack, Text } from '@shopify/polaris';
2 | import { MediaGrid } from './MediaGrid';
3 |
4 | export const Example = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 | Media
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/components/library/MediaGrid/Preview/MediaGrid.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from 'react';
2 | import { DropZone, Image, Icon, Button } from '@shopify/polaris';
3 | import { DeleteIcon, PlusIcon } from '@shopify/polaris-icons';
4 | import styles from './MediaGrid.module.css';
5 |
6 | export const MediaGrid = () => {
7 | const [images, setImages] = useState([]);
8 |
9 | const handleDrop = useCallback((_droppedFiles, acceptedFiles) => {
10 | setImages((prev) => [...prev, ...acceptedFiles?.map((file) => ({ file, id: generateId() }))]);
11 | }, []);
12 |
13 | if (images?.length) {
14 | return (
15 |
16 | {images.map((image, i) => {
17 | return (
18 |
23 |
24 |
25 | setImages((prev) => prev.toSpliced(i, 1))}
30 | >
31 |
32 |
33 |
34 |
41 |
42 | );
43 | })}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | return (
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | /*
62 | Very basic ID generator, used to prevent duplicate id errors
63 | if uploading the same image multiple times. Can swap out for
64 | something more robust or use an npm package like `uuid`
65 | */
66 | const generateId = () => {
67 | return Math.random().toString(20).slice(3);
68 | };
69 |
--------------------------------------------------------------------------------
/components/library/MediaGrid/Preview/MediaGrid.module.css:
--------------------------------------------------------------------------------
1 | .grid {
2 | display: grid;
3 | grid-template-columns: repeat(6, 1fr);
4 | grid-template-rows: repeat(auto, 1fr);
5 | gap: var(--p-space-150);
6 | width: 100%;
7 | height: 100%;
8 | margin: 0;
9 | padding: 0;
10 | }
11 |
12 | .dropZoneBox {
13 | background: var(--p-color-bg-surface-secondary);
14 | border-color: var(--p-color-border-tertiary);
15 | border-style: dashed;
16 | border-radius: var(--p-border-radius-200);
17 | border-width: var(--p-border-width-0165);
18 | display: flex;
19 | align-items: center;
20 | justify-content: center;
21 | height: 100%;
22 | width: 100%;
23 | cursor: pointer;
24 | aspect-ratio: 1;
25 | }
26 |
27 | .dropZoneBox:hover {
28 | background: var(--p-color-bg-surface-secondary-hover);
29 | }
30 |
31 | .mediaItem {
32 | border: 1px solid rgba(204, 204, 204, 1);
33 | position: relative;
34 | display: flex;
35 | border-radius: var(--p-border-radius-200);
36 | justify-content: center;
37 | align-items: center;
38 | aspect-ratio: 1;
39 | }
40 |
41 | .mediaOverlay {
42 | background-color: #00000080;
43 | opacity: 0;
44 | height: 100%;
45 | border-radius: var(--p-border-radius-200);
46 | width: 100%;
47 | position: absolute;
48 | top: 0;
49 | left: 0;
50 | transition: opacity var(--p-motion-duration-150);
51 | }
52 |
53 | .mediaOverlay:hover {
54 | opacity: 1;
55 | }
56 |
57 | .deleteButton {
58 | position: absolute;
59 | top: 5px;
60 | right: 5px;
61 | cursor: pointer;
62 | }
63 |
64 | .image {
65 | object-fit: cover;
66 | border-radius: var(--p-border-radius-200);
67 | aspect-ratio: 1;
68 | }
69 |
--------------------------------------------------------------------------------
/components/library/MediaGrid/Preview/index.ts:
--------------------------------------------------------------------------------
1 | export { Example as Preview } from "./Example";
2 |
--------------------------------------------------------------------------------
/components/library/MediaGrid/index.ts:
--------------------------------------------------------------------------------
1 | // Index files for each component directory should export the tabs, Component, and title
2 |
3 | import { Platform } from '@/types';
4 |
5 | export { tabs } from './tabs';
6 | export { Preview } from './Preview';
7 | export const title = 'Media Grid';
8 | export const contributors = [{ username: 'devwithalex', platform: Platform.TWITTER }];
9 |
--------------------------------------------------------------------------------
/components/library/MediaGrid/tabs.ts:
--------------------------------------------------------------------------------
1 | import { Tab } from '@/types';
2 |
3 | const MediaGrid = `import { useState, useCallback } from 'react';
4 | import { DropZone, Image, Icon, Button } from '@shopify/polaris';
5 | import { DeleteIcon, PlusIcon } from '@shopify/polaris-icons';
6 | import styles from './MediaGrid.module.css';
7 |
8 | export const MediaGrid = () => {
9 | const [images, setImages] = useState([]);
10 |
11 | const handleDrop = useCallback((_droppedFiles, acceptedFiles) => {
12 | setImages((prev) => [...prev, ...acceptedFiles?.map((file) => ({ file, id: generateId() }))]);
13 | }, []);
14 |
15 | if (images?.length) {
16 | return (
17 |
18 | {images.map((image, i) => {
19 | return (
20 |
25 |
26 |
27 | setImages((prev) => prev.toSpliced(i, 1))}
32 | >
33 |
34 |
35 |
36 |
43 |
44 | );
45 | })}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
56 | return (
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | /*
64 | Very basic ID generator, used to prevent duplicate id errors
65 | if uploading the same image multiple times. Can swap out for
66 | something more robust or use an npm package like \`uuid\`
67 | */
68 | const generateId = () => {
69 | return Math.random().toString(20).slice(3);
70 | };
71 | `;
72 |
73 | const Example = `import { Layout, Page, Card, BlockStack, Text } from '@shopify/polaris';
74 | import { MediaGrid } from './MediaGrid';
75 |
76 | export const Example = () => {
77 | return (
78 |
79 |
80 |
81 |
82 |
83 |
84 | Media
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | );
93 | };
94 | `;
95 |
96 | const MediaGridCSS = `.grid {
97 | display: grid;
98 | grid-template-columns: repeat(6, 1fr);
99 | grid-template-rows: repeat(auto, 1fr);
100 | gap: var(--p-space-150);
101 | width: 100%;
102 | height: 100%;
103 | margin: 0;
104 | padding: 0;
105 | }
106 |
107 | .dropZoneBox {
108 | background: var(--p-color-bg-surface-secondary);
109 | border-color: var(--p-color-border-tertiary);
110 | border-style: dashed;
111 | border-radius: var(--p-border-radius-200);
112 | border-width: var(--p-border-width-0165);
113 | display: flex;
114 | align-items: center;
115 | justify-content: center;
116 | height: 100%;
117 | width: 100%;
118 | cursor: pointer;
119 | aspect-ratio: 1;
120 | }
121 |
122 | .dropZoneBox:hover {
123 | background: var(--p-color-bg-surface-secondary-hover);
124 | }
125 |
126 | .mediaItem {
127 | border: 1px solid rgba(204, 204, 204, 1);
128 | position: relative;
129 | display: flex;
130 | border-radius: var(--p-border-radius-200);
131 | justify-content: center;
132 | align-items: center;
133 | aspect-ratio: 1;
134 | }
135 |
136 | .mediaOverlay {
137 | background-color: #00000080;
138 | opacity: 0;
139 | height: 100%;
140 | border-radius: var(--p-border-radius-200);
141 | width: 100%;
142 | position: absolute;
143 | top: 0;
144 | left: 0;
145 | transition: opacity var(--p-motion-duration-150);
146 | }
147 |
148 | .mediaOverlay:hover {
149 | opacity: 1;
150 | }
151 |
152 | .deleteButton {
153 | position: absolute;
154 | top: 5px;
155 | right: 5px;
156 | cursor: pointer;
157 | }
158 |
159 | .image {
160 | object-fit: cover;
161 | border-radius: var(--p-border-radius-200);
162 | aspect-ratio: 1;
163 | }
164 | `;
165 |
166 | export const tabs: Tab[] = [
167 | { title: 'Example Usage', content: Example },
168 | { title: 'MediaGrid.jsx', content: MediaGrid },
169 | { title: 'MediaGrid.module.css', content: MediaGridCSS }
170 | ];
171 |
--------------------------------------------------------------------------------
/components/library/NavCard/Preview/Example.jsx:
--------------------------------------------------------------------------------
1 | import { Page, Layout } from "@shopify/polaris";
2 | import { NavCard } from "./NavCard";
3 |
4 | export const Example = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/components/library/NavCard/Preview/NavCard.jsx:
--------------------------------------------------------------------------------
1 | import { Card, Text, InlineStack, Icon } from '@shopify/polaris';
2 | import { ChevronRightIcon, OrderIcon } from "@shopify/polaris-icons";
3 |
4 | export const NavCard = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 | 38 orders to fulfill
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/components/library/NavCard/Preview/index.ts:
--------------------------------------------------------------------------------
1 | export { Example as Preview } from "./Example";
2 |
--------------------------------------------------------------------------------
/components/library/NavCard/index.ts:
--------------------------------------------------------------------------------
1 | // Index files for each component directory should export the tabs, Component, and title
2 |
3 | import { Platform } from '@/types';
4 |
5 | export { tabs } from './tabs';
6 | export { Preview } from './Preview';
7 | export const title = 'Nav Card';
8 | export const contributors = [{ username: 'devwithalex', platform: Platform.TWITTER }];
9 |
--------------------------------------------------------------------------------
/components/library/NavCard/tabs.ts:
--------------------------------------------------------------------------------
1 | import { Tab } from '@/types';
2 |
3 | const NavCard = `import { Card, Text, InlineStack, Icon } from '@shopify/polaris';
4 | import { ChevronRightIcon, OrderIcon } from '@shopify/polaris-icons';
5 |
6 | export const NavCard = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 | 38 orders to fulfill
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | };
25 | `;
26 |
27 | const Example = `import { Page, Layout } from "@shopify/polaris";
28 | import { NavCard } from "./NavCard";
29 |
30 | export const Example = () => {
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | );
40 | };
41 | `;
42 |
43 | export const tabs: Tab[] = [
44 | { title: 'Example Usage', content: Example },
45 | { title: 'NavCard.jsx', content: NavCard }
46 | ];
47 |
--------------------------------------------------------------------------------
/components/library/PricingCard/Preview/Example.jsx:
--------------------------------------------------------------------------------
1 | import { InlineStack } from "@shopify/polaris";
2 | import { PricingCard } from "./PricingCard";
3 |
4 | export const Example = () => {
5 | return (
6 |
7 | console.log("clicked plan!"),
23 | },
24 | }}
25 | />
26 | console.log("clicked plan!"),
43 | },
44 | }}
45 | />
46 | console.log("clicked plan!"),
62 | },
63 | }}
64 | />
65 |
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/components/library/PricingCard/Preview/PricingCard.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | BlockStack,
3 | Card,
4 | Text,
5 | InlineStack,
6 | Box,
7 | Button,
8 | Badge,
9 | ButtonGroup
10 | } from '@shopify/polaris';
11 |
12 | export const PricingCard = ({
13 | title,
14 | description,
15 | price,
16 | features,
17 | featuredText,
18 | button,
19 | frequency
20 | }) => {
21 | return (
22 |
31 | {featuredText ? (
32 |
33 |
34 | {featuredText}
35 |
36 |
37 | ) : null}
38 |
39 |
40 |
41 |
42 | {title}
43 |
44 | {description ? (
45 |
46 | {description}
47 |
48 | ) : null}
49 |
50 |
51 |
52 |
53 | {price}
54 |
55 |
56 |
57 | / {frequency}
58 |
59 |
60 |
61 |
62 |
63 | {features?.map((feature, id) => (
64 |
65 | {feature}
66 |
67 | ))}
68 |
69 |
70 |
71 |
72 | {button.content}
73 |
74 |
75 |
76 |
77 |
78 | );
79 | };
80 |
--------------------------------------------------------------------------------
/components/library/PricingCard/Preview/index.ts:
--------------------------------------------------------------------------------
1 | export { Example as Preview } from "./Example";
2 |
--------------------------------------------------------------------------------
/components/library/PricingCard/index.ts:
--------------------------------------------------------------------------------
1 | // Index files for each component should export the tabs, preview, and title
2 |
3 | import { Platform } from '@/types';
4 |
5 | export { tabs } from './tabs';
6 | export { Preview } from './Preview';
7 | export const title = 'Pricing Card';
8 | export const contributors = [{ username: 'devwithalex', platform: Platform.TWITTER }];
9 |
--------------------------------------------------------------------------------
/components/library/PricingCard/tabs.ts:
--------------------------------------------------------------------------------
1 | import { Tab } from '@/types';
2 |
3 | const PricingCard = `import { BlockStack, Card, Text, InlineStack, Box, Button, Badge, ButtonGroup } from "@shopify/polaris";
4 |
5 | export const PricingCard = ({ title, description, price, features, featuredText, button, frequency }) => {
6 | return (
7 |
16 | {featuredText ? (
17 |
18 |
19 | {featuredText}
20 |
21 |
22 | ) : null}
23 |
24 |
25 |
26 |
27 | {title}
28 |
29 | {description ? (
30 |
31 | {description}
32 |
33 | ) : null}
34 |
35 |
36 |
37 |
38 | {price}
39 |
40 |
41 | / {frequency}
42 |
43 |
44 |
45 |
46 | {features?.map((feature, id) => (
47 |
48 | {feature}
49 |
50 | ))}
51 |
52 |
53 |
54 |
55 | {button.content}
56 |
57 |
58 |
59 |
60 |
61 | );
62 | };
63 | `;
64 |
65 | const Example = `import { InlineStack } from "@shopify/polaris";
66 | import { PricingCard } from "./PricingCard";
67 |
68 | export const Example = () => {
69 | return (
70 |
71 | console.log("clicked plan!"),
87 | },
88 | }}
89 | />
90 | console.log("clicked plan!"),
107 | },
108 | }}
109 | />
110 | console.log("clicked plan!"),
126 | },
127 | }}
128 | />
129 |
130 | );
131 | };
132 | `;
133 |
134 | export const tabs: Tab[] = [
135 | { title: 'Example Usage', content: Example },
136 | { title: 'PricingCard.jsx', content: PricingCard }
137 | ];
138 |
--------------------------------------------------------------------------------
/components/library/ReviewBanner/Preview/Example.jsx:
--------------------------------------------------------------------------------
1 | import { Page, Layout } from '@shopify/polaris';
2 | import { ReviewBanner } from './ReviewBanner';
3 |
4 | export function Example() {
5 | return (
6 |
7 | {
11 | console.log(`Rating: ${rating}`);
12 | // You can:
13 | // - Hide the banner
14 | // - Redirect to app store
15 | // - Record analytics
16 | // - Send slack notifications like `${shopName} clicked on review banner: ${rating} stars`
17 | }}
18 | onClose={() => {
19 | // Handle the close action here
20 | console.log('Review banner closed');
21 | }}
22 | />
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/components/library/ReviewBanner/Preview/ReviewBanner.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Card, BlockStack, Text, InlineStack, Button } from '@shopify/polaris';
3 | import { XIcon } from '@shopify/polaris-icons';
4 | /**
5 | * A banner component that allows users to submit reviews using a 5-star rating system.
6 | * @param {Object} props - The component props
7 | * @param {string} props.title - The title text to display in the banner
8 | * @param {string} props.description - The description text to display below the title
9 | * @param {Function} props.onReview - Callback function that receives the selected rating (1-5)
10 | * @param {Function} props.onClose - Callback function that handles the close action
11 | * @returns {JSX.Element} A card containing the review banner
12 | */
13 | export function ReviewBanner({ title, description, onReview, onClose }) {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 | {title}
21 |
22 |
23 |
24 |
25 | {description}
26 |
27 |
28 |
29 |
30 |
31 | );
32 | }
33 |
34 | /**
35 | * A star rating component that allows users to select a rating between 1 and 5 stars.
36 | * @param {Object} props - The component props
37 | * @param {Function} props.onChange - Callback function that receives the selected rating (1-5)
38 | * @returns {JSX.Element} A row of interactive star icons
39 | */
40 | function ReviewStars({ onReview }) {
41 | const [rating, setRating] = useState(0);
42 | const [hoverRating, setHoverRating] = useState(null);
43 |
44 | const handleStarClick = (index) => {
45 | const newRating = index + 1;
46 | setRating(newRating);
47 | onReview(newRating);
48 | };
49 |
50 | return (
51 |
52 | {[...Array(5)].map((_, index) => (
53 | handleStarClick(index)}
56 | onMouseEnter={() => setHoverRating(index + 1)}
57 | onMouseLeave={() => setHoverRating(null)}
58 | style={{ cursor: 'pointer', transition: 'color 0.1s' }}
59 | >
60 |
61 |
62 | ))}
63 |
64 | );
65 | }
66 |
67 | /**
68 | * A star icon component that can be filled or unfilled.
69 | * @param {Object} props - The component props
70 | * @param {boolean} props.filled - Whether the star should be filled (gold gradient) or unfilled (gray)
71 | * @param {string} props.id - Unique identifier for the gradient definition
72 | * @returns {JSX.Element} An SVG star icon
73 | */
74 | function StarIcon({ filled, id }) {
75 | return (
76 |
77 |
78 |
79 |
85 |
91 |
92 |
93 |
99 |
104 |
105 | );
106 | }
107 |
--------------------------------------------------------------------------------
/components/library/ReviewBanner/Preview/index.ts:
--------------------------------------------------------------------------------
1 | export { Example as Preview } from "./Example";
2 |
--------------------------------------------------------------------------------
/components/library/ReviewBanner/index.ts:
--------------------------------------------------------------------------------
1 | import { Platform } from '@/types';
2 |
3 | // Index files for each component should export the tabs, preview, and title
4 | export { tabs } from './tabs';
5 | export { Preview } from './Preview/index';
6 | export const title = 'Review Banner';
7 | export const contributors = [{ username: 'ghussaindars', platform: Platform.TWITTER }];
8 |
--------------------------------------------------------------------------------
/components/library/ReviewBanner/tabs.ts:
--------------------------------------------------------------------------------
1 | import { Tab } from '@/types';
2 |
3 | const Example = `import { Page, Layout } from '@shopify/polaris';
4 | import { ReviewBanner } from './ReviewBanner';
5 |
6 | export function Example() {
7 | return (
8 |
9 | {
13 | console.log(rating);
14 | // redirect user to the review page, you can also record the rating in your analytics
15 | }}
16 | onClose={() => console.log('Review banner closed')}
17 | />
18 |
19 | );
20 | }
21 |
22 | `;
23 |
24 | const ReviewBanner = `
25 | import { useState } from 'react';
26 | import { Card, BlockStack, Text, InlineStack, Button } from '@shopify/polaris';
27 | import { XIcon } from '@shopify/polaris-icons';
28 | /**
29 | * A banner component that allows users to submit reviews using a 5-star rating system.
30 | * @param {Object} props - The component props
31 | * @param {string} props.title - The title text to display in the banner
32 | * @param {string} props.description - The description text to display below the title
33 | * @param {Function} props.onReview - Callback function that receives the selected rating (1-5)
34 | * @param {Function} props.onClose - Callback function that handles the close action
35 | * @returns {JSX.Element} A card containing the review banner
36 | */
37 | export function ReviewBanner({ title, description, onReview, onClose }) {
38 | return (
39 |
40 |
41 |
42 |
43 |
44 | {title}
45 |
46 |
47 |
48 |
49 | {description}
50 |
51 |
52 |
53 |
54 |
55 | );
56 | }
57 |
58 | /**
59 | * A star rating component that allows users to select a rating between 1 and 5 stars.
60 | * @param {Object} props - The component props
61 | * @param {Function} props.onChange - Callback function that receives the selected rating (1-5)
62 | * @returns {JSX.Element} A row of interactive star icons
63 | */
64 | function ReviewStars({ onChange }) {
65 | const [rating, setRating] = useState(0);
66 | const [hoverRating, setHoverRating] = useState(null);
67 |
68 | const handleStarClick = (index) => {
69 | const newRating = index + 1;
70 | setRating(newRating);
71 | onChange(newRating);
72 | };
73 |
74 | return (
75 |
76 | {[...Array(5)].map((_, index) => (
77 | handleStarClick(index)}
80 | onMouseEnter={() => setHoverRating(index + 1)}
81 | onMouseLeave={() => setHoverRating(null)}
82 | style={{ cursor: 'pointer', transition: 'color 0.1s' }}
83 | >
84 |
85 |
86 | ))}
87 |
88 | );
89 | }
90 |
91 | /**
92 | * A star icon component that can be filled or unfilled.
93 | * @param {Object} props - The component props
94 | * @param {boolean} props.filled - Whether the star should be filled (gold gradient) or unfilled (gray)
95 | * @param {string} props.id - Unique identifier for the gradient definition
96 | * @returns {JSX.Element} An SVG star icon
97 | */
98 | function StarIcon({ filled, id }) {
99 | return (
100 |
101 |
102 |
103 |
109 |
115 |
116 |
117 |
123 |
128 |
129 | );
130 | }
131 | `;
132 |
133 | export const tabs: Tab[] = [
134 | { title: 'Example Usage', content: Example },
135 | { title: 'ReviewBanner.jsx', content: ReviewBanner }
136 | ];
137 |
--------------------------------------------------------------------------------
/components/library/RichTextEditor/Preview/Example.jsx:
--------------------------------------------------------------------------------
1 | import { BlockStack, Box, Card, Page, Text, SkeletonBodyText } from '@shopify/polaris';
2 | import { useState } from 'react';
3 | import 'react-quill/dist/quill.snow.css';
4 | import { RichTextEditor } from './RichTextEditor';
5 |
6 | export function Example() {
7 | const [textEditor1, setTextEditor1] = useState('');
8 | const [textEditor2, setTextEditor2] = useState('');
9 | const [textEditor3, setTextEditor3] = useState('');
10 | const [textEditor4, setTextEditor4] = useState('');
11 | const [textEditor5, setTextEditor5] = useState('');
12 | const [charCount, setCharCount] = useState(0);
13 |
14 | function handleChangeWithCounter(value, editor) {
15 | setCharCount(editor.getText().length);
16 | setTextEditor4(value);
17 | }
18 |
19 | return (
20 |
21 |
22 |
23 |
29 |
36 |
43 |
44 | handleChangeWithCounter(value, editor)}
48 | value={textEditor4}
49 | />
50 | Character count: {charCount}
51 |
52 |
69 |
70 | Remix ClientOnly Fallback - loading...
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/components/library/RichTextEditor/Preview/RichTextEditor.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Icon, InlineGrid, Text } from '@shopify/polaris';
2 | import { AlertCircleIcon } from '@shopify/polaris-icons';
3 | import Quill from 'react-quill';
4 | import 'react-quill/dist/quill.snow.css';
5 |
6 | const defaultModuleOptions = {
7 | toolbar: [
8 | ['bold', 'italic', 'underline', 'blockquote'],
9 | [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }],
10 | ['link'],
11 | ['clean']
12 | ],
13 | clipboard: {
14 | matchVisual: false // Needed to avoid this glitch: https://github.com/slab/quill/issues/2905
15 | }
16 | };
17 |
18 | export function RichTextEditor({
19 | bounds = '.quill',
20 | defaultValue,
21 | formats,
22 | id,
23 | modules,
24 | onBlur,
25 | onChange,
26 | onChangeSelection,
27 | onFocus,
28 | onKeyDown,
29 | onKeyPress,
30 | onKeyUp,
31 | placeholder,
32 | preserveWhitespace,
33 | value,
34 | label,
35 | labelHidden = false,
36 | disabled,
37 | error
38 | }) {
39 | const mergedModuleOptions = {
40 | ...defaultModuleOptions,
41 | ...modules,
42 | toolbar: modules?.toolbar || defaultModuleOptions.toolbar,
43 | clipboard: {
44 | ...defaultModuleOptions.clipboard,
45 | ...(modules?.clipboard || {})
46 | }
47 | };
48 |
49 | let className = disabled ? 'quill--disabled ' : '';
50 | if (error) className += 'quill--error';
51 |
52 | return (
53 |
54 | {!labelHidden && (
55 |
56 |
57 | {label}
58 |
59 |
60 | )}
61 |
81 | {error && (
82 |
83 |
84 |
85 | {error}
86 |
87 |
88 | )}
89 |
90 | );
91 | }
92 |
--------------------------------------------------------------------------------
/components/library/RichTextEditor/Preview/index.ts:
--------------------------------------------------------------------------------
1 | export { Example as Preview } from "./Example";
2 |
--------------------------------------------------------------------------------
/components/library/RichTextEditor/index.ts:
--------------------------------------------------------------------------------
1 | import { Platform } from '@/types';
2 |
3 | // Index files for each component should export the tabs, preview, and title
4 | export { tabs } from './tabs';
5 | export { Preview } from './Preview/index';
6 | export const title = 'Rich Text Editor';
7 | export const contributors = [{ username: 'fabregas4', platform: Platform.GITHUB }];
8 | export const dependencies = ['react-quill'];
9 |
--------------------------------------------------------------------------------
/components/library/RichTextEditor/tabs.ts:
--------------------------------------------------------------------------------
1 | import { Tab } from '@/types';
2 |
3 | const RichTextEditor = `import { Box, Icon, InlineGrid, Text } from '@shopify/polaris';
4 | import { AlertCircleIcon } from '@shopify/polaris-icons';
5 | import Quill from 'react-quill';
6 | import 'react-quill/dist/quill.snow.css';
7 |
8 | const defaultModuleOptions = {
9 | toolbar: [
10 | ['bold', 'italic', 'underline', 'blockquote'],
11 | [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }],
12 | ['link'],
13 | ['clean']
14 | ],
15 | clipboard: {
16 | matchVisual: false // Needed to avoid this glitch: https://github.com/slab/quill/issues/2905
17 | }
18 | };
19 |
20 | export function RichTextEditor({
21 | bounds = '.quill',
22 | defaultValue,
23 | formats,
24 | id,
25 | modules,
26 | onBlur,
27 | onChange,
28 | onChangeSelection,
29 | onFocus,
30 | onKeyDown,
31 | onKeyPress,
32 | onKeyUp,
33 | placeholder,
34 | preserveWhitespace,
35 | value,
36 | label,
37 | labelHidden = false,
38 | disabled,
39 | error
40 | }) {
41 | const mergedModuleOptions = {
42 | ...defaultModuleOptions,
43 | ...modules,
44 | toolbar: modules?.toolbar || defaultModuleOptions.toolbar,
45 | clipboard: {
46 | ...defaultModuleOptions.clipboard,
47 | ...(modules?.clipboard || {})
48 | }
49 | };
50 |
51 | let className = disabled ? 'quill--disabled ' : '';
52 | if (error) className += 'quill--error';
53 |
54 | return (
55 |
56 | {!labelHidden && (
57 |
58 |
59 | {label}
60 |
61 |
62 | )}
63 |
83 | {error && (
84 |
85 |
86 |
87 | {error}
88 |
89 |
90 | )}
91 |
92 | );
93 | }`;
94 |
95 | const Example = `import { BlockStack, Box, Card, Page, Text, SkeletonBodyText } from '@shopify/polaris';
96 | import { useState } from 'react';
97 | import 'react-quill/dist/quill.snow.css';
98 | import { RichTextEditor } from './RichTextEditor';
99 |
100 | export function Example() {
101 | const [textEditor1, setTextEditor1] = useState('');
102 | const [textEditor2, setTextEditor2] = useState('');
103 | const [textEditor3, setTextEditor3] = useState('');
104 | const [textEditor4, setTextEditor4] = useState('');
105 | const [textEditor5, setTextEditor5] = useState('');
106 | const [charCount, setCharCount] = useState(0);
107 |
108 | function handleChangeWithCounter(value, editor) {
109 | setCharCount(editor.getText().length);
110 | setTextEditor4(value);
111 | }
112 |
113 | return (
114 |
115 |
116 |
117 |
123 |
130 |
137 |
138 | handleChangeWithCounter(value, editor)}
142 | value={textEditor4}
143 | />
144 | Character count: {charCount}
145 |
146 |
163 |
164 | Remix ClientOnly Fallback - loading...
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 | );
175 | }`;
176 |
177 | const CSSFile = `/* In Remix - simply import this file from within RichTextEditor.jsx */
178 | /* In Next.js - import this file from within /pages/_app.tsx */
179 |
180 | /* In Remix - you may want to put this 1 style into a global CSS file or in a separate CSS file
181 | imported to the file referencing the RTE component, so it can also be applied to the ClientOnly fallback.
182 | See the Remix usage example to see how this might be used */
183 | .quill {
184 | background-color: var(--p-color-input-bg-surface);
185 | border: var(--p-border-width-0165) solid var(--p-color-input-border);
186 | border-top-color: #898f94;
187 | border-radius: var(--p-border-radius-200);
188 | margin-bottom: 3px;
189 | }
190 |
191 | .quill--disabled {
192 | color: var(--p-color-text-disabled);
193 | background-color: #F2F2F2;
194 | border: 1px solid #F2F2F2;
195 | }
196 |
197 | .quill .ql-toolbar.ql-snow {
198 | border: none;
199 | border-bottom: 1px solid #ccc;
200 | }
201 |
202 | .quill .ql-container.ql-snow {
203 | border: none;
204 | font-family: var(--p-font-family-sans);
205 | }
206 |
207 | .quill .ql-container.ql-snow .ql-editor {
208 | min-height: 150px;
209 | max-height: 400px;
210 | }
211 |
212 | .quill--disabled .ql-toolbar .ql-stroke {
213 | fill: none !important;
214 | stroke: #aaa !important;
215 | }
216 |
217 | .quill--disabled .ql-toolbar .ql-fill {
218 | fill: #aaa !important;
219 | stroke: none !important;
220 | }
221 |
222 | .quill--disabled .ql-toolbar .ql-picker {
223 | color: #aaa !important;
224 | }
225 |
226 | .quill--error {
227 | border: var(--p-border-width-0165) solid var(--p-color-border-critical-secondary);
228 | background-color: var(--p-color-bg-surface-critical);
229 | }
230 |
231 | .quill .ql-editor.ql-blank::before {
232 | font-style: normal;
233 | }
234 |
235 | .quill--disabled .ql-editor.ql-blank::before {
236 | color: var(--p-color-text-disabled)
237 | }
238 | `;
239 |
240 | const ExampleRemix = `/*
241 | In Remix you need to make changes to ensure that both importing and rendering of Quill isn't done on the server-side
242 | Render is prevented by remix-utils/client-only
243 | Import is prevented by naming the RichTextEditor component RichTextEditor.client.jsx
244 | Example below includes a fallback, where a skeleton is displayed if the RichTextEditor is still loading on the client
245 | */
246 |
247 | import { BlockStack, Box, Card, Page, Text, SkeletonBodyText } from '@shopify/polaris';
248 | import { useState } from 'react';
249 | import 'react-quill/dist/quill.snow.css';
250 | import { RichTextEditor } from './RichTextEditor.client';
251 | import { ClientOnly } from 'remix-utils/client-only';
252 |
253 | export default function ExampleRemix() {
254 | const [textEditor1, setTextEditor1] = useState("");
255 |
256 | return (
257 |
260 |
261 |
262 |
264 | Notification template - loading...
265 |
266 |
267 |
268 |
269 | }>
270 | {() => (
271 |
277 | )}
278 |
279 |
280 |
281 |
282 | );
283 | }
284 | `;
285 |
286 | export const tabs: Tab[] = [
287 | { title: 'Example Usage', content: Example },
288 | { title: 'RichTextEditor.jsx', content: RichTextEditor },
289 | { title: 'RichTextEditor.css', content: CSSFile, lang: 'css' },
290 | { title: 'Example Usage (Remix)', content: ExampleRemix }
291 | ];
292 |
--------------------------------------------------------------------------------
/components/library/SetupGuide/Preview/Example.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Button } from "@shopify/polaris";
3 | import { SetupGuide } from "./SetupGuide";
4 |
5 | export const Example = () => {
6 | const [showGuide, setShowGuide] = useState(true);
7 | const [items, setItems] = useState(ITEMS);
8 |
9 | // Example of step complete handler, adjust for your use case
10 | const onStepComplete = async (id) => {
11 | try {
12 | // API call to update completion state in DB, etc.
13 | await new Promise((res) =>
14 | setTimeout(() => {
15 | res();
16 | }, [1000])
17 | );
18 |
19 | setItems((prev) => prev.map((item) => (item.id === id ? { ...item, complete: !item.complete } : item)));
20 | } catch (e) {
21 | console.error(e);
22 | }
23 | };
24 |
25 | if (!showGuide) return setShowGuide(true)}>Show Setup Guide ;
26 |
27 | return (
28 |
29 | {
31 | setShowGuide(false);
32 | setItems(ITEMS);
33 | }}
34 | onStepComplete={onStepComplete}
35 | items={items}
36 | />
37 |
38 | );
39 | };
40 |
41 | // EXAMPLE DATA - COMPONENT API
42 | const ITEMS = [
43 | {
44 | id: 0,
45 | title: "Add your first product",
46 | description:
47 | "If checking out takes longer than 30 seconds, half of all shoppers quit. Let your customers check out quickly with a one-step payment solution.",
48 | image: {
49 | url: "https://cdn.shopify.com/shopifycloud/shopify/assets/admin/home/onboarding/shop_pay_task-70830ae12d3f01fed1da23e607dc58bc726325144c29f96c949baca598ee3ef6.svg",
50 | alt: "Illustration highlighting ShopPay integration",
51 | },
52 | complete: false,
53 | primaryButton: {
54 | content: "Add product",
55 | props: {
56 | url: "https://www.example.com",
57 | external: true,
58 | },
59 | },
60 | secondaryButton: {
61 | content: "Import products",
62 | props: {
63 | url: "https://www.example.com",
64 | external: true,
65 | },
66 | },
67 | },
68 | {
69 | id: 1,
70 | title: "Share your online store",
71 | description:
72 | "Drive awareness and traffic by sharing your store via SMS and email with your closest network, and on communities like Instagram, TikTok, Facebook, and Reddit.",
73 | image: {
74 | url: "https://cdn.shopify.com/shopifycloud/shopify/assets/admin/home/onboarding/detail-images/home-onboard-share-store-b265242552d9ed38399455a5e4472c147e421cb43d72a0db26d2943b55bdb307.svg",
75 | alt: "Illustration showing an online storefront with a 'share' icon in top right corner",
76 | },
77 | complete: false,
78 | primaryButton: {
79 | content: "Copy store link",
80 | props: {
81 | onAction: () => console.log("copied store link!"),
82 | },
83 | },
84 | },
85 | {
86 | id: 2,
87 | title: "Translate your store",
88 | description:
89 | "Translating your store improves cross-border conversion by an average of 13%. Add languages for your top customer regions for localized browsing, notifications, and checkout.",
90 | image: {
91 | url: "https://cdn.shopify.com/b/shopify-guidance-dashboard-public/nqjyaxwdnkg722ml73r6dmci3cpn.svgz",
92 | },
93 | complete: false,
94 | primaryButton: {
95 | content: "Add a language",
96 | props: {
97 | url: "https://www.example.com",
98 | external: true,
99 | },
100 | },
101 | },
102 | ];
103 |
--------------------------------------------------------------------------------
/components/library/SetupGuide/Preview/SetupGuide.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useId } from 'react';
2 | import {
3 | BlockStack,
4 | Card,
5 | Text,
6 | InlineStack,
7 | ButtonGroup,
8 | Button,
9 | ProgressBar,
10 | Box,
11 | Collapsible,
12 | Tooltip,
13 | Spinner,
14 | Icon,
15 | Popover,
16 | ActionList,
17 | Image
18 | } from '@shopify/polaris';
19 | import { MenuHorizontalIcon, ChevronDownIcon, ChevronUpIcon, CheckIcon, XIcon } from "@shopify/polaris-icons";
20 | import styles from './SetupGuide.module.css';
21 |
22 | export const SetupGuide = ({ onDismiss, onStepComplete, items }) => {
23 | const [expanded, setExpanded] = useState(items.findIndex((item) => !item.complete));
24 | const [isGuideOpen, setIsGuideOpen] = useState(true);
25 | const [popoverActive, setPopoverActive] = useState(false);
26 | const accessId = useId();
27 | const completedItemsLength = items.filter((item) => item.complete).length;
28 |
29 | return (
30 |
31 |
32 |
33 |
34 |
35 | Setup Guide
36 |
37 |
38 | setPopoverActive((prev) => !prev)}
41 | activator={
42 | setPopoverActive((prev) => !prev)}
44 | variant='tertiary'
45 | icon={MenuHorizontalIcon}
46 | />
47 | }
48 | >
49 |
63 |
64 |
65 | )
66 | }
67 | ]}
68 | />
69 |
70 |
71 | {
75 | setIsGuideOpen((prev) => {
76 | if (!prev) setExpanded(items.findIndex((item) => !item.complete));
77 | return !prev;
78 | });
79 | }}
80 | ariaControls={accessId}
81 | />
82 |
83 |
84 |
85 | Use this personalized guide to get your app up and running.
86 |
87 |
88 |
89 | {completedItemsLength === items.length ? (
90 |
91 |
92 |
97 |
98 | Done
99 |
100 |
101 |
102 | ) : (
103 |
104 | {`${completedItemsLength} / ${items.length} completed`}
105 |
106 | )}
107 |
108 | {completedItemsLength !== items.length ? (
109 |
110 |
item.complete).length / items.length) * 100}
112 | size='small'
113 | tone='primary'
114 | animated
115 | />
116 |
117 | ) : null}
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | {items.map((item) => {
126 | return (
127 | setExpanded(item.id)}
131 | onComplete={onStepComplete}
132 | {...item}
133 | />
134 | );
135 | })}
136 |
137 |
138 |
139 | {completedItemsLength === items.length ? (
140 |
146 |
147 | Dismiss Guide
148 |
149 |
150 | ) : null}
151 |
152 | );
153 | };
154 |
155 | const SetupItem = ({
156 | complete,
157 | onComplete,
158 | expanded,
159 | setExpanded,
160 | title,
161 | description,
162 | image,
163 | primaryButton,
164 | secondaryButton,
165 | id
166 | }) => {
167 | const [loading, setLoading] = useState(false);
168 |
169 | const completeItem = async () => {
170 | setLoading(true);
171 | await onComplete(id);
172 | setLoading(false);
173 | };
174 |
175 | return (
176 |
177 |
178 |
179 |
180 |
181 |
182 | {loading ? (
183 |
184 | ) : complete ? (
185 |
197 | ) : (
198 | outlineSvg
199 | )}
200 |
201 |
202 |
203 | null : setExpanded}
206 | style={{
207 | cursor: expanded ? 'default' : 'pointer',
208 | paddingTop: '.15rem',
209 | width: '100%'
210 | }}
211 | >
212 |
213 |
214 | {title}
215 |
216 |
217 |
218 |
219 |
220 | {description}
221 |
222 | {primaryButton || secondaryButton ? (
223 |
224 | {primaryButton ? (
225 |
226 | {primaryButton.content}
227 |
228 | ) : null}
229 | {secondaryButton ? (
230 |
231 | {secondaryButton.content}
232 |
233 | ) : null}
234 |
235 | ) : null}
236 |
237 |
238 |
239 |
240 | {image && expanded ? ( // hide image at 700px down
241 |
247 | ) : null}
248 |
249 |
250 |
251 |
252 | );
253 | };
254 |
255 | const outlineSvg = (
256 |
257 |
264 |
270 |
271 |
280 |
281 | );
282 |
--------------------------------------------------------------------------------
/components/library/SetupGuide/Preview/SetupGuide.module.css:
--------------------------------------------------------------------------------
1 | .setupItem {
2 | padding: 0.25rem 0.5rem;
3 | border-radius: 0.5rem;
4 | }
5 |
6 | .setupItem:hover {
7 | background-color: #f7f7f7;
8 | }
9 |
10 | .setupItemExpanded:hover {
11 | background-color: inherit;
12 | }
13 |
14 | .completeButton {
15 | width: 1.5rem;
16 | height: 1.5rem;
17 | display: flex;
18 | justify-content: center;
19 | align-items: center;
20 | color: #303030;
21 | }
22 |
23 | .itemContent {
24 | width: 100%;
25 | display: flex;
26 | gap: 8rem;
27 | justify-content: space-between;
28 | }
29 |
30 | /* These styles take into account the Shopify sidebar visibility & hides image based on window width */
31 | @media (min-width: 48em) and (max-width: 61.871875em) {
32 | .itemImage {
33 | display: none;
34 | }
35 | }
36 |
37 | @media (max-width: 45.625em) {
38 | .itemImage {
39 | display: none;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/components/library/SetupGuide/Preview/index.ts:
--------------------------------------------------------------------------------
1 | export { Example as Preview } from "./Example";
2 |
--------------------------------------------------------------------------------
/components/library/SetupGuide/index.ts:
--------------------------------------------------------------------------------
1 | // Index files for each component should export the tabs, preview, and title
2 |
3 | import { Platform } from '@/types';
4 |
5 | export { tabs } from './tabs';
6 | export { Preview } from './Preview';
7 | export const title = 'Setup Guide';
8 | export const contributors = [{ username: 'devwithalex', platform: Platform.TWITTER }];
9 |
--------------------------------------------------------------------------------
/components/library/SetupGuide/tabs.ts:
--------------------------------------------------------------------------------
1 | import { Tab } from '@/types';
2 |
3 | const SetupGuide =
4 | `import { useState, useId } from 'react';
5 | import {
6 | BlockStack,
7 | Card,
8 | Text,
9 | InlineStack,
10 | ButtonGroup,
11 | Button,
12 | ProgressBar,
13 | Box,
14 | Collapsible,
15 | Tooltip,
16 | Spinner,
17 | Icon,
18 | Popover,
19 | ActionList,
20 | Image
21 | } from '@shopify/polaris';
22 | import { MenuHorizontalIcon, ChevronDownIcon, ChevronUpIcon, CheckIcon, XIcon } from "@shopify/polaris-icons";
23 | import styles from './SetupGuide.module.css';
24 |
25 | export const SetupGuide = ({ onDismiss, onStepComplete, items }) => {
26 | const [expanded, setExpanded] = useState(items.findIndex((item) => !item.complete));
27 | const [isGuideOpen, setIsGuideOpen] = useState(true);
28 | const [popoverActive, setPopoverActive] = useState(false);
29 | const accessId = useId();
30 | const completedItemsLength = items.filter((item) => item.complete).length;
31 |
32 | return (
33 |
34 |
35 |
36 |
37 |
38 | Setup Guide
39 |
40 |
41 | setPopoverActive((prev) => !prev)}
44 | activator={
45 | setPopoverActive((prev) => !prev)}
47 | variant='tertiary'
48 | icon={MenuHorizontalIcon}
49 | />
50 | }
51 | >
52 |
66 |
67 |
68 | )
69 | }
70 | ]}
71 | />
72 |
73 |
74 | {
78 | setIsGuideOpen((prev) => {
79 | if (!prev) setExpanded(items.findIndex((item) => !item.complete));
80 | return !prev;
81 | });
82 | }}
83 | ariaControls={accessId}
84 | />
85 |
86 |
87 |
88 | Use this personalized guide to get your app up and running.
89 |
90 |
91 |
92 | {completedItemsLength === items.length ? (
93 |
94 |
95 |
100 |
101 | Done
102 |
103 |
104 |
105 | ) : (
106 |
107 | {` +
108 | '`${completedItemsLength} / ${items.length} completed`' +
109 | `}
110 |
111 | )}
112 |
113 | {completedItemsLength !== items.length ? (
114 |
115 |
item.complete).length / items.length) * 100}
117 | size='small'
118 | tone='primary'
119 | animated
120 | />
121 |
122 | ) : null}
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | {items.map((item) => {
131 | return (
132 | setExpanded(item.id)}
136 | onComplete={onStepComplete}
137 | {...item}
138 | />
139 | );
140 | })}
141 |
142 |
143 |
144 | {completedItemsLength === items.length ? (
145 |
151 |
152 | Dismiss Guide
153 |
154 |
155 | ) : null}
156 |
157 | );
158 | };
159 |
160 | const SetupItem = ({
161 | complete,
162 | onComplete,
163 | expanded,
164 | setExpanded,
165 | title,
166 | description,
167 | image,
168 | primaryButton,
169 | secondaryButton,
170 | id
171 | }) => {
172 | const [loading, setLoading] = useState(false);
173 |
174 | const completeItem = async () => {
175 | setLoading(true);
176 | await onComplete(id);
177 | setLoading(false);
178 | };
179 |
180 | return (
181 |
182 |
185 |
186 |
187 |
188 |
189 | {loading ? (
190 |
191 | ) : complete ? (
192 |
204 | ) : (
205 | outlineSvg
206 | )}
207 |
208 |
209 |
210 | null : setExpanded}
213 | style={{
214 | cursor: expanded ? 'default' : 'pointer',
215 | paddingTop: '.15rem',
216 | width: '100%'
217 | }}
218 | >
219 |
220 |
221 | {title}
222 |
223 |
224 |
225 |
226 |
227 | {description}
228 |
229 | {primaryButton || secondaryButton ? (
230 |
231 | {primaryButton ? (
232 |
233 | {primaryButton.content}
234 |
235 | ) : null}
236 | {secondaryButton ? (
237 |
238 | {secondaryButton.content}
239 |
240 | ) : null}
241 |
242 | ) : null}
243 |
244 |
245 |
246 |
247 | {image && expanded ? ( // hide image at 700px down
248 |
254 | ) : null}
255 |
256 |
257 |
258 |
259 | );
260 | };
261 |
262 | const outlineSvg = (
263 |
264 |
271 |
277 |
278 |
287 |
288 | );
289 | `;
290 |
291 | const SetupGuideCss = `/* If using CSS modules in Remix, make sure you have properly configured your project (https://remix.run/docs/en/main/styling/css-modules#css-modules) */
292 |
293 | .setupItem {
294 | padding: 0.25rem 0.5rem;
295 | border-radius: 0.5rem;
296 | }
297 |
298 | .setupItem:hover {
299 | background-color: #f7f7f7;
300 | }
301 |
302 | .setupItemExpanded:hover {
303 | background-color: inherit;
304 | }
305 |
306 | .completeButton {
307 | width: 1.5rem;
308 | height: 1.5rem;
309 | display: flex;
310 | justify-content: center;
311 | align-items: center;
312 | color: #303030;
313 | }
314 |
315 | .itemContent {
316 | width: 100%;
317 | display: flex;
318 | gap: 8rem;
319 | justify-content: space-between;
320 | }
321 |
322 | /* These styles take into account the Shopify sidebar visibility & hides image based on window width */
323 | @media (min-width: 48em) and (max-width: 61.871875em) {
324 | .itemImage {
325 | display: none;
326 | }
327 | }
328 |
329 | @media (max-width: 45.625em) {
330 | .itemImage {
331 | display: none;
332 | }
333 | }
334 | `;
335 |
336 | const Example = `import { useState } from "react";
337 | import { Button } from "@shopify/polaris";
338 | import { SetupGuide } from "./SetupGuide";
339 |
340 | export const Example = () => {
341 | const [showGuide, setShowGuide] = useState(true);
342 | const [items, setItems] = useState(ITEMS);
343 |
344 | // Example of step complete handler, adjust for your use case
345 | const onStepComplete = async (id) => {
346 | try {
347 | // API call to update completion state in DB, etc.
348 | await new Promise((res) =>
349 | setTimeout(() => {
350 | res();
351 | }, [1000])
352 | );
353 |
354 | setItems((prev) => prev.map((item) => (item.id === id ? { ...item, complete: !item.complete } : item)));
355 | } catch (e) {
356 | console.error(e);
357 | }
358 | };
359 |
360 | if (!showGuide) return setShowGuide(true)}>Show Setup Guide ;
361 |
362 | return (
363 |
364 | {
366 | setShowGuide(false);
367 | setItems(ITEMS);
368 | }}
369 | onStepComplete={onStepComplete}
370 | items={items}
371 | />
372 |
373 | );
374 | };
375 |
376 | // EXAMPLE DATA - COMPONENT API
377 | const ITEMS = [
378 | {
379 | id: 0,
380 | title: "Add your first product",
381 | description:
382 | "If checking out takes longer than 30 seconds, half of all shoppers quit. Let your customers check out quickly with a one-step payment solution.",
383 | image: {
384 | url: "https://cdn.shopify.com/shopifycloud/shopify/assets/admin/home/onboarding/shop_pay_task-70830ae12d3f01fed1da23e607dc58bc726325144c29f96c949baca598ee3ef6.svg",
385 | alt: "Illustration highlighting ShopPay integration",
386 | },
387 | complete: false,
388 | primaryButton: {
389 | content: "Add product",
390 | props: {
391 | url: "https://www.example.com",
392 | external: true,
393 | },
394 | },
395 | secondaryButton: {
396 | content: "Import products",
397 | props: {
398 | url: "https://www.example.com",
399 | external: true,
400 | },
401 | },
402 | },
403 | {
404 | id: 1,
405 | title: "Share your online store",
406 | description:
407 | "Drive awareness and traffic by sharing your store via SMS and email with your closest network, and on communities like Instagram, TikTok, Facebook, and Reddit.",
408 | image: {
409 | url: "https://cdn.shopify.com/shopifycloud/shopify/assets/admin/home/onboarding/detail-images/home-onboard-share-store-b265242552d9ed38399455a5e4472c147e421cb43d72a0db26d2943b55bdb307.svg",
410 | alt: "Illustration showing an online storefront with a 'share' icon in top right corner",
411 | },
412 | complete: false,
413 | primaryButton: {
414 | content: "Copy store link",
415 | props: {
416 | onAction: () => console.log("copied store link!"),
417 | },
418 | },
419 | },
420 | {
421 | id: 2,
422 | title: "Translate your store",
423 | description:
424 | "Translating your store improves cross-border conversion by an average of 13%. Add languages for your top customer regions for localized browsing, notifications, and checkout.",
425 | image: {
426 | url: "https://cdn.shopify.com/b/shopify-guidance-dashboard-public/nqjyaxwdnkg722ml73r6dmci3cpn.svgz",
427 | },
428 | complete: false,
429 | primaryButton: {
430 | content: "Add a language",
431 | props: {
432 | url: "https://www.example.com",
433 | external: true,
434 | },
435 | },
436 | },
437 | ];
438 | `;
439 |
440 | export const tabs: Tab[] = [
441 | { title: 'Example Usage', content: Example },
442 | { title: 'SetupGuide.jsx', content: SetupGuide },
443 | { title: 'SetupGuide.module.css', content: SetupGuideCss, lang: 'css' }
444 | ];
445 |
--------------------------------------------------------------------------------
/components/library/SortableList/Preview/Example.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Page, Layout } from '@shopify/polaris';
3 | import { SortableList } from './SortableList';
4 |
5 | export const Example = () => {
6 | const [items, setItems] = useState([
7 | { id: 1, title: 'T-Shirt', status: 'active' },
8 | { id: 2, title: 'Skateboard', status: 'active' },
9 | { id: 3, title: 'Snowboard', status: 'archived' },
10 | { id: 4, title: 'Ultimate Snowboard', status: 'active' },
11 | { id: 5, title: 'Mechanical Pencil', status: 'draft' }
12 | ]);
13 |
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/components/library/SortableList/Preview/SortableList.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | BlockStack,
3 | Card,
4 | ResourceList,
5 | Text,
6 | ResourceItem,
7 | Avatar,
8 | Box,
9 | InlineStack,
10 | Badge,
11 | Button
12 | } from '@shopify/polaris';
13 | import { DragHandleIcon, XIcon } from "@shopify/polaris-icons";
14 | import {
15 | DndContext,
16 | closestCenter,
17 | KeyboardSensor,
18 | PointerSensor,
19 | useSensor,
20 | useSensors
21 | } from '@dnd-kit/core';
22 | import {
23 | arrayMove,
24 | SortableContext,
25 | sortableKeyboardCoordinates,
26 | verticalListSortingStrategy,
27 | useSortable
28 | } from '@dnd-kit/sortable';
29 | import { restrictToVerticalAxis, restrictToParentElement } from '@dnd-kit/modifiers';
30 | import styles from './SortableList.module.css';
31 |
32 | const Item = ({ id, title, status }) => {
33 | const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
34 | id
35 | });
36 |
37 | const tone = status === 'active' ? 'success' : status === 'draft' ? 'info' : undefined;
38 |
39 | const style = {
40 | ...(transform
41 | ? {
42 | transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
43 | transition
44 | }
45 | : {}),
46 | zIndex: isDragging ? 1000 : 0,
47 | position: 'relative'
48 | };
49 |
50 | return (
51 |
52 |
console.log('Handle item click')}
56 | >
57 |
58 |
59 | {/* Build your own implementation of the ResourceItem, but preserve this drag handle div as the first item in the InlineStack */}
60 | e.stopPropagation()}
64 | className={styles.itemAction}
65 | style={{ touchAction: 'none', display: 'flex' }} // Prevents page scrolling on mobile touch
66 | >
67 |
68 |
69 | {/* Don't use `media` prop of ResourceItem, if you need to you can place your Avatar or Image here instead after the DragHandler */}
70 |
71 |
72 | {title}
73 |
74 |
75 |
76 |
77 | {`${status.charAt(0).toUpperCase()}${status.slice(1)}`}
78 |
79 | {
83 | e.stopPropagation();
84 | console.log('Remove Item');
85 | }}
86 | >
87 |
88 |
89 |
90 |
91 |
92 |
93 | );
94 | };
95 |
96 | export const SortableList = ({ items, setItems }) => {
97 | const sensors = useSensors(
98 | useSensor(PointerSensor),
99 | useSensor(KeyboardSensor, {
100 | coordinateGetter: sortableKeyboardCoordinates
101 | })
102 | );
103 |
104 | const handleDragEnd = (event) => {
105 | const { active, over } = event;
106 |
107 | if (active.id !== over.id) {
108 | // Updates items in state, add additional update handler logic here (e.g. API calls, toasts, etc.)
109 | setItems((items) => {
110 | const oldIndex = items.findIndex((item) => item.id === active.id);
111 | const newIndex = items.findIndex((item) => item.id === over.id);
112 | const updatedItems = arrayMove(items, oldIndex, newIndex);
113 |
114 | return updatedItems;
115 | });
116 | }
117 | };
118 |
119 | return (
120 |
121 |
122 |
123 |
124 | Sortable Products
125 |
126 |
127 |
133 |
134 | {
138 | return ;
139 | }}
140 | />
141 |
142 |
143 |
144 |
145 | );
146 | };
147 |
--------------------------------------------------------------------------------
/components/library/SortableList/Preview/SortableList.module.css:
--------------------------------------------------------------------------------
1 | /* If using CSS modules in Remix, make sure you have configured CSS bundling (https://remix.run/docs/en/main/styling/bundling) */
2 |
3 | .itemAction {
4 | opacity: 0.6;
5 | }
6 |
7 | .itemAction:hover {
8 | opacity: 1;
9 | }
10 |
--------------------------------------------------------------------------------
/components/library/SortableList/Preview/index.ts:
--------------------------------------------------------------------------------
1 | export { Example as Preview } from "./Example";
2 |
--------------------------------------------------------------------------------
/components/library/SortableList/index.ts:
--------------------------------------------------------------------------------
1 | // Index files for each component should export the tabs, preview, and title
2 |
3 | import { Platform } from '@/types';
4 |
5 | export { tabs } from './tabs';
6 | export { Preview } from './Preview';
7 | export const title = 'Sortable List';
8 | export const contributors = [{ username: 'devwithalex', platform: Platform.TWITTER }];
9 | export const dependencies = ['@dnd-kit/core', '@dnd-kit/sortable', '@dnd-kit/modifiers'];
10 |
--------------------------------------------------------------------------------
/components/library/SortableList/tabs.ts:
--------------------------------------------------------------------------------
1 | import { Tab } from '@/types';
2 |
3 | const SortableList =
4 | `import {
5 | BlockStack,
6 | Card,
7 | ResourceList,
8 | Text,
9 | ResourceItem,
10 | Avatar,
11 | Box,
12 | InlineStack,
13 | Badge,
14 | Button
15 | } from '@shopify/polaris';
16 | import { DragHandleIcon, XIcon } from '@shopify/polaris-icons';
17 | import {
18 | DndContext,
19 | closestCenter,
20 | KeyboardSensor,
21 | PointerSensor,
22 | useSensor,
23 | useSensors
24 | } from '@dnd-kit/core';
25 | import {
26 | arrayMove,
27 | SortableContext,
28 | sortableKeyboardCoordinates,
29 | verticalListSortingStrategy,
30 | useSortable
31 | } from '@dnd-kit/sortable';
32 | import { restrictToVerticalAxis, restrictToParentElement } from '@dnd-kit/modifiers';
33 | import styles from './SortableList.module.css';
34 |
35 | const Item = ({ id, title, status }) => {
36 | const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
37 | id
38 | });
39 |
40 | const tone = status === 'active' ? 'success' : status === 'draft' ? 'info' : undefined;
41 |
42 | const style = {
43 | ...(transform
44 | ? {
45 | transform: ` +
46 | '`translate3d(${transform.x}px, ${transform.y}px, 0)`' +
47 | `,
48 | transition
49 | }
50 | : {}),
51 | zIndex: isDragging ? 1000 : 0,
52 | position: 'relative'
53 | };
54 |
55 | return (
56 |
57 |
console.log('Handle item click')}
63 | >
64 |
65 |
66 | {/* Build your own implementation of the ResourceItem, but preserve this drag handle div as the first item in the InlineStack */}
67 | e.stopPropagation()}
71 | className={styles.itemAction}
72 | style={{ touchAction: 'none' }} // Prevents page scrolling on mobile touch
73 | >
74 |
75 |
76 | {/* Don't use ` +
77 | '`media`' +
78 | ` prop of ResourceItem, if you need to you can place your Avatar or Image here instead after the DragHandler */}
79 |
80 |
81 | {title}
82 |
83 |
84 |
85 |
86 | {` +
87 | '`${status.charAt(0).toUpperCase()}${status.slice(1)}`' +
88 | `}
89 |
90 | {
94 | e.stopPropagation();
95 | console.log('Remove Item');
96 | }}
97 | >
98 |
99 |
100 |
101 |
102 |
103 |
104 | );
105 | };
106 |
107 | export const SortableList = ({ items, setItems }) => {
108 | const sensors = useSensors(
109 | useSensor(PointerSensor),
110 | useSensor(KeyboardSensor, {
111 | coordinateGetter: sortableKeyboardCoordinates
112 | })
113 | );
114 |
115 | const handleDragEnd = (event) => {
116 | const { active, over } = event;
117 |
118 | if (active.id !== over.id) {
119 | // Updates items in state, add additional update handler logic here (e.g. API calls, toasts, etc.)
120 | setItems((items) => {
121 | const oldIndex = items.findIndex((item) => item.id === active.id);
122 | const newIndex = items.findIndex((item) => item.id === over.id);
123 | const updatedItems = arrayMove(items, oldIndex, newIndex);
124 |
125 | return updatedItems;
126 | });
127 | }
128 | };
129 |
130 | return (
131 |
132 |
133 |
134 |
135 | Sortable Products
136 |
137 |
138 |
144 |
145 | {
149 | return ;
150 | }}
151 | />
152 |
153 |
154 |
155 |
156 | );
157 | };
158 | `;
159 |
160 | const SortableListCss = `/* If using CSS modules in Remix, make sure you have configured CSS bundling (https://remix.run/docs/en/main/styling/bundling) */
161 |
162 | .itemAction {
163 | opacity: 0.6;
164 | }
165 |
166 | .itemAction:hover {
167 | opacity: 1;
168 | }
169 | `;
170 |
171 | const Example = `import { useState } from 'react';
172 | import { Page, Layout } from '@shopify/polaris';
173 | import { SortableList } from './SortableList';
174 |
175 | export const Example = () => {
176 | const [items, setItems] = useState([
177 | { id: 1, title: 'T-Shirt', status: 'active' },
178 | { id: 2, title: 'Skateboard', status: 'active' },
179 | { id: 3, title: 'Snowboard', status: 'archived' },
180 | { id: 4, title: 'Ultimate Snowboard', status: 'active' },
181 | { id: 5, title: 'Mechanical Pencil', status: 'draft' }
182 | ]);
183 |
184 | return (
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 | );
193 | };
194 | `;
195 |
196 | export const tabs: Tab[] = [
197 | { title: 'Example Usage', content: Example },
198 | { title: 'SortableList.jsx', content: SortableList },
199 | { title: 'SortableList.module.css', content: SortableListCss, lang: 'css' }
200 | ];
201 |
--------------------------------------------------------------------------------
/components/library/StatBox/Preview/Example.jsx:
--------------------------------------------------------------------------------
1 | import { Layout, Page, Grid, Text, BlockStack, Box } from '@shopify/polaris';
2 | import { StatBox } from './StatBox';
3 |
4 | export const Example = () => {
5 | // Each array represents the values for the past 7 days including today
6 | const stats = {
7 | orders: [13, 20, 18, 5, 8, 15, 23],
8 | reviews: [3, 3, 5, 6, 5, 2, 8],
9 | returns: [5, 6, 5, 8, 4, 3, 1]
10 | };
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
18 | Daily Stats Example
19 |
20 | Shows rate of change from first entry of chart data to today
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/components/library/StatBox/Preview/StatBox.jsx:
--------------------------------------------------------------------------------
1 | // IMPORTANT: If using this component in Remix, you must wrap it in the component from the remix-utils package
2 |
3 | import { Card, Text, Box } from '@shopify/polaris';
4 | import { ArrowUpIcon, ArrowDownIcon } from '@shopify/polaris-icons';
5 | import { SparkLineChart } from '@shopify/polaris-viz';
6 | import '@shopify/polaris-viz/build/esm/styles.css';
7 |
8 | export const StatBox = ({ title, value, data = [] }) => {
9 | const hasData = data && data.length;
10 | const percentageChange = hasData
11 | ? getPercentageChange(Number(data[0]), Number(data.at(-1)))
12 | : null;
13 |
14 | return (
15 |
16 |
17 |
26 |
34 |
42 |
43 | {title}
44 |
45 |
46 |
47 | {value}
48 |
49 |
50 | {percentageChange ? (
51 | percentageChange > 0 ? (
52 |
53 | ) : (
54 |
55 | )
56 | ) : null}
57 |
58 | 0 ? 'green' : 'red'
63 | }
64 | : undefined
65 | }
66 | >
67 | {Math.abs(percentageChange) || '-'}%
68 |
69 |
70 |
71 |
72 | {hasData ? (
73 |
74 |
75 |
76 | ) : null}
77 |
78 |
79 |
80 | );
81 | };
82 |
83 | // Formats number array to expected format from polaris-viz chart
84 | const formatChartData = (values = []) => {
85 | return [{ data: values?.map((stat, idx) => ({ key: idx, value: stat })) }];
86 | };
87 |
88 | // Gets rate of change based on first + last entry in chart data
89 | const getPercentageChange = (start = 0, end = 0) => {
90 | if (isNaN(start) || isNaN(end)) return null;
91 |
92 | const percentage = (((end - start) / start) * 100).toFixed(0);
93 |
94 | if (percentage > 999) {
95 | return 999;
96 | }
97 |
98 | if (percentage < -999) {
99 | return -999;
100 | }
101 |
102 | return percentage;
103 | };
104 |
--------------------------------------------------------------------------------
/components/library/StatBox/Preview/index.ts:
--------------------------------------------------------------------------------
1 | export { Example as Preview } from "./Example";
2 |
--------------------------------------------------------------------------------
/components/library/StatBox/index.ts:
--------------------------------------------------------------------------------
1 | // Index files for each component directory should export the tabs, Component, and title
2 |
3 | import { Platform } from '@/types';
4 |
5 | export { tabs } from './tabs';
6 | export { Preview } from './Preview';
7 | export const title = 'Stat Box';
8 | export const contributors = [{ username: 'devwithalex', platform: Platform.TWITTER }];
9 | export const dependencies = ['@shopify/polaris-viz'];
10 |
--------------------------------------------------------------------------------
/components/library/StatBox/tabs.ts:
--------------------------------------------------------------------------------
1 | import { Tab } from '@/types';
2 |
3 | const StatBox = `// IMPORTANT: If using this component in Remix, you must wrap it in the component from the remix-utils package
4 |
5 | import { Card, Text, Box } from '@shopify/polaris';
6 | import { ArrowUpIcon, ArrowDownIcon } from '@shopify/polaris-icons';
7 | import { SparkLineChart } from '@shopify/polaris-viz';
8 | import '@shopify/polaris-viz/build/esm/styles.css';
9 |
10 | export const StatBox = ({ title, value, data = [] }) => {
11 | const hasData = data && data.length;
12 | const percentageChange = hasData
13 | ? getPercentageChange(Number(data[0]), Number(data.at(-1)))
14 | : null;
15 |
16 | return (
17 |
18 |
19 |
28 |
36 |
44 |
45 | {title}
46 |
47 |
48 |
49 | {value}
50 |
51 |
52 | {percentageChange ? (
53 | percentageChange > 0 ? (
54 |
55 | ) : (
56 |
57 | )
58 | ) : null}
59 |
60 | 0 ? 'green' : 'red'
65 | }
66 | : undefined
67 | }
68 | >
69 | {Math.abs(percentageChange) || '-'}%
70 |
71 |
72 |
73 |
74 | {hasData ? (
75 |
76 |
77 |
78 | ) : null}
79 |
80 |
81 |
82 | );
83 | };
84 |
85 | // Formats number array to expected format from polaris-viz chart
86 | const formatChartData = (values = []) => {
87 | return [{ data: values?.map((stat, idx) => ({ key: idx, value: stat })) }];
88 | };
89 |
90 | // Gets rate of change based on first + last entry in chart data
91 | const getPercentageChange = (start = 0, end = 0) => {
92 | if (isNaN(start) || isNaN(end)) return null;
93 |
94 | const percentage = (((end - start) / start) * 100).toFixed(0);
95 |
96 | if (percentage > 999) {
97 | return 999;
98 | }
99 |
100 | if (percentage < -999) {
101 | return -999;
102 | }
103 |
104 | return percentage;
105 | };
106 | `;
107 |
108 | const Example = `import { Layout, Page, Grid, Text, BlockStack, Box } from '@shopify/polaris';
109 | import { StatBox } from './StatBox';
110 |
111 | export const Example = () => {
112 | // Each array represents the values for the past 7 days including today
113 | const stats = {
114 | orders: [13, 20, 18, 5, 8, 15, 23],
115 | reviews: [3, 3, 5, 6, 5, 2, 8],
116 | returns: [5, 6, 5, 8, 4, 3, 1]
117 | };
118 |
119 | return (
120 |
121 |
122 |
123 |
124 |
125 | Daily Stats Example
126 |
127 | Shows rate of change from first entry of chart data to today
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 | );
148 | };
149 | `;
150 |
151 | export const tabs: Tab[] = [
152 | { title: 'Example Usage', content: Example },
153 | { title: 'StatBox.jsx', content: StatBox }
154 | ];
155 |
--------------------------------------------------------------------------------
/components/library/Timeline/Preview/Example.jsx:
--------------------------------------------------------------------------------
1 | import { Page, Badge, Layout, Box, Card } from '@shopify/polaris';
2 | import Timeline from './Timeline';
3 |
4 | export function Example() {
5 | // Event timestamps must be in date order (ascending or descending)
6 | // tone is optional
7 | // - critical and caution will use Alert indicator
8 | // - success will use Check indicator
9 | // - all other tones will use Chevron indicator with Polaris tone color applied
10 | // - undefined will show the Shopify-style timeline marker
11 | // url is optional
12 | // icon is optional
13 | // timelineEvent will accept a string or a JSX.Element
14 |
15 | const timelineItems = [
16 | {
17 | tone: 'base',
18 | url: undefined,
19 | timelineEvent: (
20 | <>
21 | A refund was processed for order #1242 . An ARN was generated - 23587235897.
22 | >
23 | ),
24 | timestamp: new Date('2024-09-12T13:30:00')
25 | },
26 | {
27 | tone: 'base',
28 | icon: ,
29 | url: undefined,
30 | timelineEvent: (
31 | <>
32 | Order #1241 was successfully delivered. (Ricemill)
33 | >
34 | ),
35 | timestamp: new Date('2024-09-12T09:29:00')
36 | },
37 | {
38 | tone: 'critical',
39 | url: undefined,
40 | timelineEvent: (
41 | <>
42 | Order #1240 flagged for review due to suspicious activity.
43 | >
44 | ),
45 | timestamp: new Date('2024-09-11T15:00:00')
46 | },
47 | {
48 | tone: 'success',
49 | url: 'https://example.com/order/1235',
50 | timelineEvent: (
51 | <>
52 | Order #1235 shipped via Fedex.
53 | >
54 | ),
55 | timestamp: new Date('2024-09-11T14:59:00')
56 | },
57 | {
58 | tone: 'base',
59 | url: undefined,
60 | timelineEvent: <>Customer logged in.>,
61 | timestamp: new Date('2024-09-11T09:44:00')
62 | },
63 | {
64 | tone: 'base',
65 | url: undefined,
66 | timelineEvent: <>Failed login attempt detected.>,
67 | timestamp: new Date('2024-09-11T06:59:00')
68 | },
69 | {
70 | tone: 'base',
71 | url: undefined,
72 | icon: ,
73 | timelineEvent: (
74 | <>
75 | Customer redeemed 50 reward points on an order #1237 (LoyaltyPlus)
76 | >
77 | ),
78 | timestamp: new Date('2024-09-10T18:19:00')
79 | },
80 | {
81 | tone: 'base',
82 | url: undefined,
83 | icon: ,
84 | timelineEvent: (
85 | <>Customer earned 100 reward points for subscribing to your mailing list. (LoyaltyPlus)>
86 | ),
87 | timestamp: new Date('2024-09-10T18:14:00')
88 | },
89 | {
90 | tone: 'caution',
91 | url: undefined,
92 | timelineEvent: <>Account flagged for unusual activity.>,
93 | timestamp: new Date('2024-09-10T16:00:00')
94 | },
95 | {
96 | tone: 'base',
97 | url: 'https://example.com/fraud-check',
98 | timelineEvent: (
99 | <>
100 | Fraud check was initiated for order #1236
101 | >
102 | ),
103 | timestamp: new Date('2024-09-10T12:10:00')
104 | },
105 | {
106 | tone: 'base',
107 | url: undefined,
108 | timelineEvent: (
109 | <>
110 | Customer placed an order #1234
111 | >
112 | ),
113 | timestamp: new Date('2024-09-10T10:30:00')
114 | },
115 | {
116 | tone: 'base',
117 | url: undefined,
118 | timelineEvent: (
119 | <>
120 | Customer placed order #1239
121 | >
122 | ),
123 | timestamp: new Date('2024-09-09T13:25:00')
124 | },
125 | {
126 | tone: 'base',
127 | url: 'https://example.com/points-earned',
128 | timelineEvent: <>Customer earned 200 reward points after purchase.>,
129 | timestamp: new Date('2024-09-09T13:00:00')
130 | },
131 | {
132 | tone: 'base',
133 | url: undefined,
134 | timelineEvent: <>Customer earned 100 reward points.>,
135 | timestamp: new Date('2024-09-09T11:30:00')
136 | },
137 | {
138 | tone: 'base',
139 | url: undefined,
140 | timelineEvent: <>Customer contacted support regarding an issue with order.>,
141 | timestamp: new Date('2024-09-09T11:00:00')
142 | },
143 | {
144 | tone: 'critical',
145 | url: undefined,
146 | icon: ,
147 | timelineEvent: <>Customer flagged for fraud. (Securité)>,
148 | timestamp: new Date('2024-09-09T08:15:00')
149 | },
150 | {
151 | tone: 'base',
152 | url: undefined,
153 | icon: ,
154 | timelineEvent: <>Customer updated their shipping address. (Ricemill)>,
155 | timestamp: new Date('2024-09-08T14:09:00')
156 | },
157 | {
158 | tone: 'base',
159 | url: undefined,
160 | timelineEvent: <>Customer's email address was updated.>,
161 | timestamp: new Date('2024-09-08T12:44:00')
162 | },
163 | {
164 | tone: 'base',
165 | url: 'https://example.com/account-updated',
166 | timelineEvent: <>Account details updated.>,
167 | timestamp: new Date('2024-09-08T11:19:00')
168 | },
169 | {
170 | tone: 'base',
171 | url: undefined,
172 | timelineEvent: <>Customer subscribed to newsletter.>,
173 | timestamp: new Date('2024-09-07T09:29:00')
174 | }
175 | ];
176 |
177 | return (
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 | );
186 | }
187 |
--------------------------------------------------------------------------------
/components/library/Timeline/Preview/Timeline.jsx:
--------------------------------------------------------------------------------
1 | import { BlockStack, Box, Icon, InlineGrid, InlineStack, Link, Text } from '@shopify/polaris';
2 | import { Fragment } from 'react';
3 | import {
4 | AlertCircleIcon,
5 | CheckCircleIcon,
6 | ChevronRightIcon,
7 | CircleChevronRightIcon
8 | } from '@shopify/polaris-icons';
9 | import styles from './timeline.module.css';
10 |
11 | export default function Timeline({ items }) {
12 | function getBulletIconFromTone(tone) {
13 | switch (tone) {
14 | case 'critical':
15 | case 'caution':
16 | return AlertCircleIcon;
17 | case 'success':
18 | return CheckCircleIcon;
19 | case 'base':
20 | case undefined:
21 | return null;
22 | default:
23 | return CircleChevronRightIcon;
24 | }
25 | }
26 |
27 | let lastDate = null;
28 |
29 | return (
30 |
31 |
32 | {items?.length ? (
33 | items.map((item, index) => {
34 | const currentDate = item.timestamp.toLocaleDateString([], {
35 | year: 'numeric',
36 | month: 'long',
37 | day: 'numeric'
38 | });
39 | const showDate = currentDate !== lastDate;
40 | lastDate = currentDate;
41 | const bulletIcon = getBulletIconFromTone(item.tone);
42 |
43 | return (
44 |
45 | {showDate && (
46 |
47 |
48 |
49 |
50 |
51 | {currentDate}
52 |
53 |
54 |
55 |
56 |
57 | )}
58 |
59 |
60 |
61 | {item.tone === 'base' || !bulletIcon ? (
62 |
65 | ) : (
66 |
67 |
68 |
69 | )}
70 |
71 |
72 |
73 | {item.icon}
74 | {item.url ? (
75 |
76 |
77 |
78 | {item.timelineEvent}
79 |
80 |
81 |
82 |
83 | ) : (
84 | {item.timelineEvent}
85 | )}
86 |
87 |
88 |
89 | {item.timestamp.toLocaleTimeString([], {
90 | hour: 'numeric',
91 | minute: '2-digit',
92 | hour12: true
93 | })}
94 |
95 |
96 |
97 | );
98 | })
99 | ) : (
100 | No timeline events available.
101 | )}
102 |
103 |
104 | );
105 | }
106 |
--------------------------------------------------------------------------------
/components/library/Timeline/Preview/index.ts:
--------------------------------------------------------------------------------
1 | export { Example as Preview } from "./Example";
2 |
--------------------------------------------------------------------------------
/components/library/Timeline/Preview/timeline.module.css:
--------------------------------------------------------------------------------
1 | /* You may need to tweak the three z-index values in your app, if you find content is hiding */
2 |
3 | .timeline-icon {
4 | position: relative;
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | height: 24px;
9 | width: 24px;
10 | margin-right: 10px;
11 | }
12 |
13 | .timeline-icon-polaris-icon {
14 | z-index: 1;
15 | }
16 |
17 | .timeline-icon:before {
18 | content: '';
19 | position: absolute;
20 | top: -40px;
21 | bottom: -30px;
22 | left: 50%;
23 | transform: translateX(-50%);
24 | width: 2px;
25 | background-color: #dfe3e8;
26 | z-index: 0;
27 | }
28 |
29 | .timeline-icon-base {
30 | display: flex;
31 | justify-content: center;
32 | align-items: center;
33 | width: 16px;
34 | height: 16px;
35 | background-color: var(--p-color-border);
36 | border-radius: 3px;
37 | z-index: 1;
38 | }
39 |
40 | .timeline-icon-base-inner {
41 | width: 8px;
42 | height: 8px;
43 | background-color: var(--p-color-icon);
44 | border-radius: 3px;
45 | }
46 |
47 | .timeline-event-description .Polaris-Icon {
48 | display: inline;
49 | margin: 0;
50 | }
51 |
52 | a:hover .timeline-event-link-main {
53 | text-decoration: underline !important;
54 | }
55 |
--------------------------------------------------------------------------------
/components/library/Timeline/index.ts:
--------------------------------------------------------------------------------
1 | import { Platform } from '@/types';
2 |
3 | // Index files for each component should export the tabs, preview, and title
4 | export { tabs } from './tabs';
5 | export { Preview } from './Preview/index';
6 | export const title = 'Timeline';
7 | export const contributors = [{ username: 'fabregas4', platform: Platform.GITHUB }];
8 |
--------------------------------------------------------------------------------
/components/library/Timeline/tabs.ts:
--------------------------------------------------------------------------------
1 | import { Tab } from '@/types';
2 |
3 | const Example = `import { Page, Badge, Layout, Box, Card } from '@shopify/polaris';
4 | import Timeline from './Timeline';
5 |
6 | export function Example() {
7 | // Event timestamps must be in date order (ascending or descending)
8 | // tone is optional
9 | // - critical and caution will use Alert indicator
10 | // - success will use Check indicator
11 | // - all other tones will use Chevron indicator with Polaris tone color applied
12 | // - undefined will show the Shopify-style timeline marker
13 | // url is optional
14 | // icon is optional
15 | // timelineEvent will accept a string or a JSX.Element
16 |
17 | const timelineItems = [
18 | {
19 | tone: 'base',
20 | url: undefined,
21 | timelineEvent: (
22 | <>
23 | A refund was processed for order #1242 . An ARN was generated - 23587235897.
24 | >
25 | ),
26 | timestamp: new Date('2024-09-12T13:30:00')
27 | },
28 | {
29 | tone: 'base',
30 | icon: ,
31 | url: undefined,
32 | timelineEvent: (
33 | <>
34 | Order #1241 was successfully delivered. (Ricemill)
35 | >
36 | ),
37 | timestamp: new Date('2024-09-12T09:29:00')
38 | },
39 | {
40 | tone: 'critical',
41 | url: undefined,
42 | timelineEvent: (
43 | <>
44 | Order #1240 flagged for review due to suspicious activity.
45 | >
46 | ),
47 | timestamp: new Date('2024-09-11T15:00:00')
48 | },
49 | {
50 | tone: 'success',
51 | url: 'https://example.com/order/1235',
52 | timelineEvent: (
53 | <>
54 | Order #1235 shipped via Fedex.
55 | >
56 | ),
57 | timestamp: new Date('2024-09-11T14:59:00')
58 | },
59 | {
60 | tone: 'base',
61 | url: undefined,
62 | timelineEvent: <>Customer logged in.>,
63 | timestamp: new Date('2024-09-11T09:44:00')
64 | },
65 | {
66 | tone: 'base',
67 | url: undefined,
68 | timelineEvent: <>Failed login attempt detected.>,
69 | timestamp: new Date('2024-09-11T06:59:00')
70 | },
71 | {
72 | tone: 'base',
73 | url: undefined,
74 | icon: ,
75 | timelineEvent: (
76 | <>
77 | Customer redeemed 50 reward points on an order #1237 (LoyaltyPlus)
78 | >
79 | ),
80 | timestamp: new Date('2024-09-10T18:19:00')
81 | },
82 | {
83 | tone: 'base',
84 | url: undefined,
85 | icon: ,
86 | timelineEvent: (
87 | <>Customer earned 100 reward points for subscribing to your mailing list. (LoyaltyPlus)>
88 | ),
89 | timestamp: new Date('2024-09-10T18:14:00')
90 | },
91 | {
92 | tone: 'caution',
93 | url: undefined,
94 | timelineEvent: <>Account flagged for unusual activity.>,
95 | timestamp: new Date('2024-09-10T16:00:00')
96 | },
97 | {
98 | tone: 'base',
99 | url: 'https://example.com/fraud-check',
100 | timelineEvent: (
101 | <>
102 | Fraud check was initiated for order #1236
103 | >
104 | ),
105 | timestamp: new Date('2024-09-10T12:10:00')
106 | },
107 | {
108 | tone: 'base',
109 | url: undefined,
110 | timelineEvent: (
111 | <>
112 | Customer placed an order #1234
113 | >
114 | ),
115 | timestamp: new Date('2024-09-10T10:30:00')
116 | },
117 | {
118 | tone: 'base',
119 | url: undefined,
120 | timelineEvent: (
121 | <>
122 | Customer placed order #1239
123 | >
124 | ),
125 | timestamp: new Date('2024-09-09T13:25:00')
126 | },
127 | {
128 | tone: 'base',
129 | url: 'https://example.com/points-earned',
130 | timelineEvent: <>Customer earned 200 reward points after purchase.>,
131 | timestamp: new Date('2024-09-09T13:00:00')
132 | },
133 | {
134 | tone: 'base',
135 | url: undefined,
136 | timelineEvent: <>Customer earned 100 reward points.>,
137 | timestamp: new Date('2024-09-09T11:30:00')
138 | },
139 | {
140 | tone: 'base',
141 | url: undefined,
142 | timelineEvent: <>Customer contacted support regarding an issue with order.>,
143 | timestamp: new Date('2024-09-09T11:00:00')
144 | },
145 | {
146 | tone: 'critical',
147 | url: undefined,
148 | icon: ,
149 | timelineEvent: <>Customer flagged for fraud. (Securité)>,
150 | timestamp: new Date('2024-09-09T08:15:00')
151 | },
152 | {
153 | tone: 'base',
154 | url: undefined,
155 | icon: ,
156 | timelineEvent: <>Customer updated their shipping address. (Ricemill)>,
157 | timestamp: new Date('2024-09-08T14:09:00')
158 | },
159 | {
160 | tone: 'base',
161 | url: undefined,
162 | timelineEvent: <>Customer's email address was updated.>,
163 | timestamp: new Date('2024-09-08T12:44:00')
164 | },
165 | {
166 | tone: 'base',
167 | url: 'https://example.com/account-updated',
168 | timelineEvent: <>Account details updated.>,
169 | timestamp: new Date('2024-09-08T11:19:00')
170 | },
171 | {
172 | tone: 'base',
173 | url: undefined,
174 | timelineEvent: <>Customer subscribed to newsletter.>,
175 | timestamp: new Date('2024-09-07T09:29:00')
176 | }
177 | ];
178 |
179 | return (
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 | );
188 | }
189 | `;
190 |
191 | const Timeline = `import { BlockStack, Box, Icon, InlineGrid, InlineStack, Link, Text } from '@shopify/polaris';
192 | import { Fragment } from 'react';
193 | import {
194 | AlertCircleIcon,
195 | CheckCircleIcon,
196 | ChevronRightIcon,
197 | CircleChevronRightIcon
198 | } from '@shopify/polaris-icons';
199 | import styles from './timeline.module.css';
200 |
201 | export default function Timeline({ items }) {
202 | function getBulletIconFromTone(tone) {
203 | switch (tone) {
204 | case 'critical':
205 | case 'caution':
206 | return AlertCircleIcon;
207 | case 'success':
208 | return CheckCircleIcon;
209 | case 'base':
210 | case undefined:
211 | return null;
212 | default:
213 | return CircleChevronRightIcon;
214 | }
215 | }
216 |
217 | let lastDate = null;
218 |
219 | return (
220 |
221 |
222 | {items?.length ? (
223 | items.map((item, index) => {
224 | const currentDate = item.timestamp.toLocaleDateString([], {
225 | year: 'numeric',
226 | month: 'long',
227 | day: 'numeric'
228 | });
229 | const showDate = currentDate !== lastDate;
230 | lastDate = currentDate;
231 | const bulletIcon = getBulletIconFromTone(item.tone);
232 |
233 | return (
234 |
235 | {showDate && (
236 |
237 |
238 |
239 |
240 |
241 | {currentDate}
242 |
243 |
244 |
245 |
246 |
247 | )}
248 |
249 |
250 |
251 | {item.tone === 'base' || !bulletIcon ? (
252 |
255 | ) : (
256 |
257 |
258 |
259 | )}
260 |
261 |
262 |
263 | {item.icon}
264 | {item.url ? (
265 |
266 |
267 |
268 | {item.timelineEvent}
269 |
270 |
271 |
272 |
273 | ) : (
274 | {item.timelineEvent}
275 | )}
276 |
277 |
278 |
279 | {item.timestamp.toLocaleTimeString([], {
280 | hour: 'numeric',
281 | minute: '2-digit',
282 | hour12: true
283 | })}
284 |
285 |
286 |
287 | );
288 | })
289 | ) : (
290 | No timeline events available.
291 | )}
292 |
293 |
294 | );
295 | }
296 | `;
297 |
298 | const CSSFile = `/* You may need to tweak the three z-index values in your app, if you find content is hiding */
299 |
300 | .timeline-icon {
301 | position: relative;
302 | display: flex;
303 | align-items: center;
304 | justify-content: center;
305 | height: 24px;
306 | width: 24px;
307 | margin-right: 10px;
308 | }
309 |
310 | .timeline-icon-polaris-icon {
311 | z-index: 1;
312 | }
313 |
314 | .timeline-icon:before {
315 | content: '';
316 | position: absolute;
317 | top: -40px;
318 | bottom: -30px;
319 | left: 50%;
320 | transform: translateX(-50%);
321 | width: 2px;
322 | background-color: #dfe3e8;
323 | z-index: 0;
324 | }
325 |
326 | .timeline-icon-base {
327 | display: flex;
328 | justify-content: center;
329 | align-items: center;
330 | width: 16px;
331 | height: 16px;
332 | background-color: var(--p-color-border);
333 | border-radius: 3px;
334 | z-index: 1;
335 | }
336 |
337 | .timeline-icon-base-inner {
338 | width: 8px;
339 | height: 8px;
340 | background-color: var(--p-color-icon);
341 | border-radius: 3px;
342 | }
343 |
344 | .timeline-event-description .Polaris-Icon {
345 | display: inline;
346 | margin: 0;
347 | }
348 |
349 | a:hover .timeline-event-link-main {
350 | text-decoration: underline !important;
351 | }
352 | `;
353 |
354 | export const tabs: Tab[] = [
355 | { title: 'Example Usage', content: Example },
356 | { title: 'Timeline.jsx', content: Timeline },
357 | { title: 'timeline.module.css', content: CSSFile, lang: 'css' }
358 | ];
359 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true
4 | };
5 |
6 | module.exports = nextConfig;
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "polaris-components",
3 | "version": "0.1.0",
4 | "private": true,
5 | "license": "MIT",
6 | "author": {
7 | "name": "RAAbbott",
8 | "url": "https://www.x.com/devwithalex"
9 | },
10 | "scripts": {
11 | "dev": "next dev",
12 | "build": "next build",
13 | "start": "next start",
14 | "lint": "next lint"
15 | },
16 | "dependencies": {
17 | "@dnd-kit/core": "^6.1.0",
18 | "@dnd-kit/modifiers": "^7.0.0",
19 | "@dnd-kit/sortable": "^8.0.0",
20 | "@dnd-kit/utilities": "^3.2.2",
21 | "@shopify/polaris": "^13.4.0",
22 | "@shopify/polaris-icons": "^9.1.0",
23 | "@shopify/polaris-viz": "^14.4.0",
24 | "@types/node": "20.4.9",
25 | "@types/react": "18.2.47",
26 | "@types/react-dom": "18.2.7",
27 | "@vercel/analytics": "^1.1.1",
28 | "autoprefixer": "10.4.14",
29 | "eslint": "8.46.0",
30 | "eslint-config-next": "13.4.13",
31 | "jsx-to-string": "^1.4.0",
32 | "mixpanel-browser": "^2.48.1",
33 | "next": "^13.4.13",
34 | "postcss": "8.4.27",
35 | "react": "^18.2.0",
36 | "react-code-blocks": "^0.1.3",
37 | "react-dom": "^18.2.0",
38 | "react-element-to-jsx-string": "^15.0.0",
39 | "react-live": "^4.1.5",
40 | "react-quill": "^2.0.0",
41 | "tailwindcss": "3.3.3",
42 | "typescript": "5.1.6"
43 | },
44 | "packageManager": "yarn@4.0.2"
45 | }
46 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import '@/styles/globals.css';
2 | import '@shopify/polaris/build/esm/styles.css';
3 | import enTranslations from '@shopify/polaris/locales/en.json';
4 | import type { AppProps } from 'next/app';
5 | import { AppProvider } from '@shopify/polaris';
6 | import { Layout } from '@/components/Layout';
7 | import "../styles/rich-text-editor.css";
8 | import Head from 'next/head';
9 |
10 | export default function App({ Component, pageProps }: AppProps) {
11 | return (
12 | <>
13 |
14 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | >
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Html, Main, NextScript, Head } from "next/document";
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/pages/components/[component].tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useRouter } from 'next/router';
3 | import { RenderComponent } from '@/components/RenderComponent';
4 | import { PageComponent } from '@/types';
5 |
6 | const formatRouteToComponent = (componentName: string) => {
7 | return (
8 | componentName
9 | // Split the string by hyphens
10 | .split('-')
11 | // Capitalize the first letter of each segment
12 | .map((segment: string) => segment.charAt(0).toUpperCase() + segment.slice(1))
13 | // Join the segments back together
14 | .join('')
15 | );
16 | };
17 |
18 | export default function Component() {
19 | const router = useRouter();
20 | const { component } = router.query;
21 | const [pageComponent, setPageComponent] = useState(null);
22 |
23 | useEffect(() => {
24 | if (component) {
25 | const loadComponent = async () => {
26 | try {
27 | const componentName = formatRouteToComponent(component as string);
28 | const Component = await import(`@/components/library/${componentName}`);
29 | setPageComponent(Component);
30 | } catch (err) {
31 | console.error('Component not found:', err);
32 | }
33 | };
34 |
35 | loadComponent();
36 | }
37 | }, [component]);
38 |
39 | if (!pageComponent) {
40 | return null; // Todo: add a fallback UI here if no component found
41 | }
42 |
43 | return ;
44 | }
45 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, useBreakpoints } from "@shopify/polaris";
2 | import { useRouter } from "next/router";
3 |
4 | export default function Home() {
5 | const router = useRouter();
6 | const breakpoints = useBreakpoints();
7 | return (
8 |
9 |
14 | Polaris Components
15 |
16 |
21 | A collection of components for Shopify app developers, based on the Polaris UI library & design system
22 |
23 | router.push("/components/setup-guide")}>
24 | Start Exploring
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RAAbbott/polaris-components/0b36e71dc4c9ea8efb23f05ea920dd0e5e5a1121/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RAAbbott/polaris-components/0b36e71dc4c9ea8efb23f05ea920dd0e5e5a1121/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RAAbbott/polaris-components/0b36e71dc4c9ea8efb23f05ea920dd0e5e5a1121/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RAAbbott/polaris-components/0b36e71dc4c9ea8efb23f05ea920dd0e5e5a1121/public/logo.png
--------------------------------------------------------------------------------
/public/timeline-icon_loyalty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RAAbbott/polaris-components/0b36e71dc4c9ea8efb23f05ea920dd0e5e5a1121/public/timeline-icon_loyalty.png
--------------------------------------------------------------------------------
/public/timeline-icon_ricemill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RAAbbott/polaris-components/0b36e71dc4c9ea8efb23f05ea920dd0e5e5a1121/public/timeline-icon_ricemill.png
--------------------------------------------------------------------------------
/public/timeline-icon_security.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RAAbbott/polaris-components/0b36e71dc4c9ea8efb23f05ea920dd0e5e5a1121/public/timeline-icon_security.png
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap');
6 |
7 | pre {
8 | counter-reset: token-line;
9 | }
10 |
11 | .token-line::before {
12 | counter-increment: token-line;
13 | content: counter(token-line);
14 | width: 2rem;
15 | display: inline-block;
16 | text-align: right;
17 | padding-right: 10px;
18 | }
19 |
--------------------------------------------------------------------------------
/styles/rich-text-editor.css:
--------------------------------------------------------------------------------
1 | /* In Remix - simply import this file from within RichTextEditor.tsx */
2 | /* In Next.js - import this file from within /pages/_app.tsx */
3 | .quill {
4 | background-color: var(--p-color-input-bg-surface);
5 | border: var(--p-border-width-0165) solid var(--p-color-input-border);
6 | border-top-color: #898f94;
7 | border-radius: var(--p-border-radius-200);
8 | margin-bottom: 3px;
9 | }
10 |
11 | .quill--disabled {
12 | color: var(--p-color-text-disabled);
13 | background-color: #F2F2F2;
14 | border: 1px solid #F2F2F2;
15 | }
16 |
17 | .quill .ql-toolbar.ql-snow {
18 | border: none;
19 | border-bottom: 1px solid #ccc;
20 | }
21 |
22 | .quill .ql-container.ql-snow {
23 | border: none;
24 | font-family: var(--p-font-family-sans);
25 | }
26 |
27 | .quill .ql-container.ql-snow .ql-editor {
28 | min-height: 150px;
29 | max-height: 400px;
30 | }
31 |
32 | .quill--disabled .ql-toolbar .ql-stroke {
33 | fill: none !important;
34 | stroke: #aaa !important;
35 | }
36 |
37 | .quill--disabled .ql-toolbar .ql-fill {
38 | fill: #aaa !important;
39 | stroke: none !important;
40 | }
41 |
42 | .quill--disabled .ql-toolbar .ql-picker {
43 | color: #aaa !important;
44 | }
45 |
46 | .quill--error {
47 | border: var(--p-border-width-0165) solid var(--p-color-border-critical-secondary);
48 | background-color: var(--p-color-bg-surface-critical);
49 | }
50 |
51 | .quill .ql-editor.ql-blank::before {
52 | font-style: normal;
53 | }
54 |
55 | .quill--disabled .ql-editor.ql-blank::before {
56 | color: var(--p-color-text-disabled)
57 | }
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | const config: Config = {
4 | content: [
5 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | corePlugins: {
10 | preflight: false
11 | },
12 | theme: {
13 | extend: {
14 | backgroundImage: {
15 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
16 | 'gradient-conic':
17 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
18 | },
19 | },
20 | },
21 | plugins: [],
22 | }
23 | export default config
24 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "bundler",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "paths": {
18 | "@/*": ["./*"]
19 | }
20 | },
21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.jsx"],
22 | "exclude": ["node_modules"]
23 | }
24 |
--------------------------------------------------------------------------------
/types.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | export type Tab = {
4 | title: string; // Tab title
5 | content: string; // Text content for editor
6 | lang?: string; // Language used in code editor, defaults to 'jsx'
7 | };
8 |
9 | export type PageComponent = {
10 | Preview: () => ReactNode; // Component preview rendered in top section
11 | tabs: Tab[];
12 | title: string;
13 | subtitle?: string;
14 | Banner?: () => ReactNode; // Optional banner exported from components to provide context (e.g. external deps)
15 | contributors?: Contributor[];
16 | dependencies?: string[];
17 | };
18 |
19 | export enum UserEventType {
20 | TOUCH = 'touch',
21 | MOUSE = 'mouse'
22 | }
23 |
24 | export enum Platform {
25 | TWITTER = 'twitter',
26 | GITHUB = 'github'
27 | }
28 |
29 | export type Contributor = {
30 | username: string;
31 | platform: Platform;
32 | };
33 |
--------------------------------------------------------------------------------