├── components ├── index.js ├── Marketplace.js ├── Invitations.js └── ShopTable.js ├── mutations ├── index.js └── inviteShopOwner.js ├── screenshot.png ├── graphql └── queries │ └── shops.js ├── client └── index.js ├── README.md └── package.json /components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Marketplace } from "./Marketplace"; 2 | -------------------------------------------------------------------------------- /mutations/index.js: -------------------------------------------------------------------------------- 1 | export { default as inviteShopOwner } from "./inviteShopOwner.js"; 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/outgrow/reaction-marketplace-ui/HEAD/screenshot.png -------------------------------------------------------------------------------- /mutations/inviteShopOwner.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export default gql` 4 | mutation inviteShopOwner($input: InviteShopOwnerInput!) { 5 | inviteShopOwner(input: $input) { 6 | wasInviteSent 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /graphql/queries/shops.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export default gql` 4 | query shops($first: ConnectionLimitInt, $last: ConnectionLimitInt, $offset: Int) { 5 | shops(first: $first, last: $last, offset: $offset) { 6 | pageInfo { 7 | endCursor 8 | startCursor 9 | hasNextPage 10 | hasPreviousPage 11 | } 12 | nodes { 13 | _id 14 | createdAt 15 | name 16 | owner { 17 | emailRecords { 18 | address 19 | } 20 | } 21 | productCount 22 | } 23 | } 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withApollo } from "react-apollo"; 3 | import Shopping from "mdi-material-ui/Shopping"; 4 | import { registerOperatorRoute } from "/imports/client/ui"; 5 | import { Marketplace } from "../components"; 6 | 7 | registerOperatorRoute({ 8 | group: "navigation", 9 | mainComponent: Marketplace, 10 | hocs: [ 11 | withApollo 12 | ], 13 | path: "/marketplace", 14 | // eslint-disable-next-line react/display-name 15 | SidebarIconComponent: (props) => , 16 | sidebarI18nLabel: "marketplaceSettings.sidebarLabel", 17 | shouldShowSidebarLink: (currentShop) => currentShop.shopType === "primary" 18 | }); 19 | -------------------------------------------------------------------------------- /components/Marketplace.js: -------------------------------------------------------------------------------- 1 | import React, { useState, Fragment } from "react"; 2 | import Tab from "@material-ui/core/Tab"; 3 | import Tabs from "@material-ui/core/Tabs"; 4 | import PrimaryAppBar from "/imports/client/ui/components/PrimaryAppBar/PrimaryAppBar"; 5 | import Invitations from "./Invitations"; 6 | import ShopTable from "./ShopTable"; 7 | 8 | export default function Marketplace() { 9 | const [currentTab, setCurrentTab] = useState(0); 10 | 11 | return ( 12 | 13 | 14 | 15 | setCurrentTab(value)}> 16 | 17 | 18 | 19 | 20 | {currentTab === 0 && 21 | 22 | } 23 | 24 | {currentTab === 1 && 25 | 26 | } 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reaction-marketplace-ui 2 | 3 | A user interface for [`@outgrow/reaction-marketplace`](https://github.com/outgrow/reaction-marketplace). 4 | 5 | ![Screenshot of the UI](https://raw.githubusercontent.com/outgrow/reaction-marketplace-ui/master/screenshot.png) 6 | 7 | ## How to use 8 | 9 | Open a terminal in your `reaction-admin` project's `/imports/plugins/custom` directory. 10 | 11 | Clone the plugin: 12 | 13 | ```bash 14 | git clone https://github.com/outgrow/reaction-marketplace-ui 15 | ``` 16 | 17 | Restart `reaction-admin`. You should now see the "Marketplace" link in your sidebar on [localhost:4080](https://localhost:4080). 18 | 19 | ## Help 20 | 21 | Need help integrating this plugin into your Reaction Commerce project? Simply looking for expert [Reaction Commerce developers](https://outgrow.io)? Want someone to train your team to use Reaction at its fullest? 22 | 23 | Whether it is just a one-hour consultation to get you set up or helping your team ship a whole project from start to finish, you can't go wrong by reaching out to us: 24 | 25 | * +1 (281) OUT-GROW 26 | * contact@outgrow.io 27 | * https://outgrow.io 28 | -------------------------------------------------------------------------------- /components/Invitations.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useMutation } from "@apollo/react-hooks"; 3 | import {Card, CardActions, CardContent, CardHeader, Grid, makeStyles} from "@material-ui/core"; 4 | import Button from "@reactioncommerce/catalyst/Button"; 5 | import Toast from "@reactioncommerce/catalyst/Toast"; 6 | import { Components } from "@reactioncommerce/reaction-components"; 7 | import { inviteShopOwner } from "../mutations"; 8 | import InvitationTable from "/imports/plugins/core/accounts/client/components/Invitations.js"; 9 | 10 | const useStyles = makeStyles(() => ({ 11 | marginTop: { 12 | marginTop: "1rem" 13 | }, 14 | rightAligned: { 15 | textAlign: "right" 16 | } 17 | })); 18 | 19 | export default function Invitations() { 20 | const classes = useStyles(); 21 | 22 | const [isToastOpen, setIsToastOpen] = useState(false); 23 | const [shopOwnerInviteEmail, setShopOwnerInviteEmail] = useState(""); 24 | const [shopOwnerName, setShopOwnerName] = useState(""); 25 | const [toastMessage, setToastMessage] = useState(""); 26 | const [toastVariant, setToastVariant] = useState("info"); 27 | 28 | const [inviteShopOwnerMutation] = useMutation(inviteShopOwner, { 29 | onCompleted() { 30 | setIsToastOpen(true); 31 | setToastMessage(payload.wasInviteSent ? "Invite sent." : "Couldn't send invite."); 32 | setToastVariant(payload.wasInviteSent ? "success" : "error"); 33 | }, 34 | onError() { 35 | setIsToastOpen(true); 36 | setToastMessage(err.message); 37 | setToastVariant("error"); 38 | } 39 | }); 40 | 41 | const handleInviteShopOwner = async () => { 42 | await inviteShopOwnerMutation({ 43 | variables: { 44 | input: { 45 | emailAddress: shopOwnerInviteEmail, 46 | name: shopOwnerName 47 | } 48 | } 49 | }); 50 | }; 51 | 52 | return ( 53 | 54 | 55 | 56 | 57 | 58 | setShopOwnerInviteEmail(event.target.value)} 63 | /> 64 | setShopOwnerName(event.target.value)} 69 | /> 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | setIsToastOpen(false)} 88 | message={toastMessage} 89 | variant={toastVariant} 90 | /> 91 | 92 | ); 93 | } 94 | 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reaction-marketplace-ui", 3 | "version": "0.1.0", 4 | "description": "A user interface for @outgrowio/reaction-marketplace", 5 | "main": "client/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "out:grow", 10 | "license": "MIT", 11 | "dependencies": { 12 | "mdi-material-ui": "^6.11.0" 13 | }, 14 | "babel": { 15 | "plugins": [ 16 | [ 17 | "@babel/plugin-proposal-class-properties", 18 | { 19 | "loose": false 20 | } 21 | ], 22 | [ 23 | "lodash", 24 | { 25 | "id": [ 26 | "lodash", 27 | "recompose" 28 | ] 29 | } 30 | ], 31 | [ 32 | "module-resolver", 33 | { 34 | "root": [ 35 | "./" 36 | ], 37 | "alias": { 38 | "@reactioncommerce/api-utils": "./imports/utils", 39 | "@reactioncommerce/reaction-collections": "./imports/plugins/core/collections", 40 | "@reactioncommerce/reaction-components": "./imports/plugins/core/components/lib", 41 | "@reactioncommerce/reaction-router": "./imports/plugins/core/router/lib", 42 | "@reactioncommerce/reaction-ui": "./imports/plugins/core/ui/client/components", 43 | "underscore": "lodash" 44 | } 45 | } 46 | ] 47 | ], 48 | "env": { 49 | "test": { 50 | "presets": [ 51 | [ 52 | "@babel/env", 53 | { 54 | "targets": { 55 | "node": "current" 56 | } 57 | } 58 | ], 59 | [ 60 | "@babel/preset-react" 61 | ] 62 | ], 63 | "plugins": [ 64 | "rewire-exports", 65 | "babel-plugin-inline-import", 66 | [ 67 | "@babel/plugin-proposal-decorators", 68 | { 69 | "legacy": true 70 | } 71 | ], 72 | "@babel/plugin-proposal-function-sent", 73 | "@babel/plugin-proposal-export-namespace-from", 74 | "@babel/plugin-proposal-numeric-separator", 75 | "@babel/plugin-proposal-throw-expressions", 76 | "@babel/plugin-syntax-dynamic-import", 77 | "@babel/plugin-syntax-import-meta", 78 | [ 79 | "@babel/plugin-proposal-class-properties", 80 | { 81 | "loose": false 82 | } 83 | ], 84 | "@babel/plugin-proposal-json-strings", 85 | [ 86 | "module-resolver", 87 | { 88 | "root": [ 89 | "./" 90 | ], 91 | "alias": { 92 | "@reactioncommerce/api-utils": "./imports/utils", 93 | "@reactioncommerce/reaction-collections": "./imports/plugins/core/collections", 94 | "@reactioncommerce/reaction-components": "./imports/plugins/core/components/lib", 95 | "@reactioncommerce/reaction-router": "./imports/plugins/core/router/lib", 96 | "@reactioncommerce/reaction-ui": "./imports/plugins/core/ui/client/components", 97 | "underscore": "lodash" 98 | } 99 | } 100 | ] 101 | ] 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /components/ShopTable.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useCallback, useMemo, useState } from "react"; 2 | import i18next from "i18next"; 3 | import { useSnackbar } from "notistack"; 4 | import {Card, CardContent, CardHeader, Grid, makeStyles} from "@material-ui/core"; 5 | import DataTable, { useDataTable } from "@reactioncommerce/catalyst/DataTable"; 6 | import { useApolloClient } from "@apollo/react-hooks"; 7 | import OrderDateCell from "/imports/plugins/core/orders/client/components/DataTable/OrderDateCell"; 8 | import shopsQuery from "../graphql/queries/shops.js"; 9 | 10 | const useStyles = makeStyles(() => ({ 11 | marginTop: { 12 | marginTop: "1rem" 13 | } 14 | })); 15 | 16 | function ShopTable() { 17 | const classes = useStyles(); 18 | 19 | const apolloClient = useApolloClient(); 20 | const { enqueueSnackbar } = useSnackbar(); 21 | 22 | const [isLoading, setIsLoading] = useState(false); 23 | const [pageCount, setPageCount] = useState(1); 24 | const [tableData, setTableData] = useState([]); 25 | 26 | const columns = useMemo(() => [ 27 | { 28 | Header: i18next.t("marketplaceSettings.table.headers.id"), 29 | accessor: "_id", 30 | // eslint-disable-next-line react/no-multi-comp,react/display-name,react/prop-types 31 | Cell: ({ row }) => {row.values._id} 32 | }, 33 | { 34 | Header: i18next.t("marketplaceSettings.table.headers.date"), 35 | accessor: "createdAt", 36 | // eslint-disable-next-line react/no-multi-comp,react/display-name,react/prop-types 37 | Cell: ({ row }) => 38 | }, 39 | { 40 | Header: i18next.t("marketplaceSettings.table.headers.name"), 41 | accessor: "name", 42 | // eslint-disable-next-line react/no-multi-comp,react/display-name,react/prop-types 43 | Cell: ({ row }) => {row.values.name} 44 | }, 45 | { 46 | Header: i18next.t("marketplaceSettings.table.headers.owner"), 47 | accessor: "owner", 48 | // eslint-disable-next-line react/no-multi-comp,react/display-name,react/prop-types 49 | Cell: ({ row }) => {row.values.owner?.emailRecords[0]?.address} 50 | }, 51 | { 52 | Header: i18next.t("marketplaceSettings.table.headers.productCount"), 53 | accessor: "productCount", 54 | // eslint-disable-next-line react/no-multi-comp,react/display-name,react/prop-types 55 | Cell: ({ row }) => {row.values.productCount} 56 | } 57 | ], []); 58 | 59 | const onFetchData = useCallback(async ({ pageIndex, pageSize }) => { 60 | // Wait for shop id to be available before fetching orders. 61 | setIsLoading(true); 62 | 63 | const { data, error } = await apolloClient.query({ 64 | query: shopsQuery, 65 | variables: { 66 | first: pageSize, 67 | offset: pageIndex * pageSize 68 | }, 69 | fetchPolicy: "network-only" 70 | }); 71 | 72 | if (error && error.length) { 73 | enqueueSnackbar(i18next.t("admin.table.error", { variant: "error" })); 74 | return; 75 | } 76 | 77 | // Update the state with the fetched data as an array of objects and the calculated page count 78 | setTableData(data.shops.nodes); 79 | setPageCount(Math.ceil(data.shops.totalCount / pageSize)); 80 | 81 | setIsLoading(false); 82 | }, [apolloClient, enqueueSnackbar]); 83 | 84 | const dataTableProps = useDataTable({ 85 | columns, 86 | data: tableData, 87 | pageCount, 88 | onFetchData, 89 | getRowId: (row) => row.referenceId 90 | }); 91 | 92 | return ( 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | ); 104 | } 105 | 106 | export default ShopTable; 107 | --------------------------------------------------------------------------------