├── .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 |
177 | 178 |
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 |
205 |
210 | 211 |
212 |
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 |
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 | 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 | 227 | 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 | 229 | 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 | 23 | 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 | 25 | 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 | 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 | 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 | 31 |
32 |
33 | 34 | Media image 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 | 33 |
34 |
35 | 36 | Media image 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 | 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 | 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 | ; 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 |
65 | ) 66 | } 67 | ]} 68 | /> 69 | 70 | 71 | 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 | 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 | 228 | ) : null} 229 | {secondaryButton ? ( 230 | 233 | ) : null} 234 | 235 | ) : null} 236 | 237 | 238 | 239 | 240 | {image && expanded ? ( // hide image at 700px down 241 | {image.alt} 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 | 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 | 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 | 235 | ) : null} 236 | {secondaryButton ? ( 237 | 240 | ) : null} 241 | 242 | ) : null} 243 | 244 | 245 | 246 | 247 | {image && expanded ? ( // hide image at 700px down 248 | {image.alt} 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 ; 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 |
63 |
64 |
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 |
253 |
254 |
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 | 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 | --------------------------------------------------------------------------------