diff --git a/package.json b/package.json index 7737a6e..c51424b 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@improbable-eng/grpc-web": "^0.13.0", "browser-headers": "^0.4.1", "chroma-js": "^2.1.0", + "fuse.js": "^6.4.2", "google-protobuf": "^3.13.0", "next": "^9.5.5", "react": "^17.0.1", @@ -90,4 +91,4 @@ "tsconfig-paths-webpack-plugin": "^3.3.0", "typescript": "^4.0.3" } -} \ No newline at end of file +} diff --git a/src/common/utils/featureFlags/go/featureflags.go b/src/common/utils/featureFlags/go/featureflags.go new file mode 100644 index 0000000..5059f58 --- /dev/null +++ b/src/common/utils/featureFlags/go/featureflags.go @@ -0,0 +1 @@ +package featureflags diff --git a/src/common/utils/featureFlags/react/BUILD.bazel b/src/common/utils/featureFlags/react/BUILD.bazel new file mode 100644 index 0000000..19da708 --- /dev/null +++ b/src/common/utils/featureFlags/react/BUILD.bazel @@ -0,0 +1,7 @@ +load("//hack/bazel/js:react.bzl", "react_library") + +package(default_visibility = ["//visibility:public"]) + +react_library( + name = "react", +) diff --git a/src/common/utils/featureFlags/react/FeatureFlags.tsx b/src/common/utils/featureFlags/react/FeatureFlags.tsx new file mode 100644 index 0000000..d19dace --- /dev/null +++ b/src/common/utils/featureFlags/react/FeatureFlags.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; + +export enum FeatureFlag { + AllowListsBlockLists = 'AllowListsBlockLists', +} + +export class FeatureFlagProvider { + activeFlags: FeatureFlag[] = []; + + constructor(flags: FeatureFlag[] = []) { + this.activeFlags = flags; + } + + has(flag: FeatureFlag) { + return this.activeFlags.includes(flag); + } +} + +export const FeatureFlagsContext = React.createContext(new FeatureFlagProvider()); diff --git a/src/common/utils/featureFlags/react/index.ts b/src/common/utils/featureFlags/react/index.ts new file mode 100644 index 0000000..b91e46a --- /dev/null +++ b/src/common/utils/featureFlags/react/index.ts @@ -0,0 +1 @@ +export * from './FeatureFlags'; diff --git a/src/common/utils/featureFlags/react/storyDecorator.tsx b/src/common/utils/featureFlags/react/storyDecorator.tsx new file mode 100644 index 0000000..c7ba445 --- /dev/null +++ b/src/common/utils/featureFlags/react/storyDecorator.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { FeatureFlag, FeatureFlagProvider, FeatureFlagsContext } from './FeatureFlags'; + +export const FeatureFlagDecorator = (flags: FeatureFlag[]) => ( + storyFn: () => React.ReactNode +) => { + return ( + + {storyFn()} + + ); +}; diff --git a/src/common/utils/withContext/BUILD.bazel b/src/common/utils/withContext/BUILD.bazel index 25d55db..3a151cb 100644 --- a/src/common/utils/withContext/BUILD.bazel +++ b/src/common/utils/withContext/BUILD.bazel @@ -4,6 +4,4 @@ package(default_visibility = ["//visibility:public"]) react_library( name = "withContext", - deps = [ - ], ) diff --git a/src/design-system/atoms/feature-gate/BUILD.bazel b/src/design-system/atoms/feature-gate/BUILD.bazel new file mode 100644 index 0000000..6796cd2 --- /dev/null +++ b/src/design-system/atoms/feature-gate/BUILD.bazel @@ -0,0 +1,10 @@ +load("//hack/bazel/js:react.bzl", "react_library") + +package(default_visibility = ["//visibility:public"]) + +react_library( + name = "feature-gate", + deps = [ + "//src/common/utils/featureFlags/react", + ], +) diff --git a/src/design-system/atoms/feature-gate/FeatureGate.stories.tsx b/src/design-system/atoms/feature-gate/FeatureGate.stories.tsx new file mode 100644 index 0000000..1b90b13 --- /dev/null +++ b/src/design-system/atoms/feature-gate/FeatureGate.stories.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { FeatureFlagDecorator } from 'roleypoly/src/common/utils/featureFlags/react/storyDecorator'; +import { FeatureGate } from './FeatureGate'; + +export default { + title: 'Atoms/Feature Gate', + decorators: [FeatureFlagDecorator(['AllowListBlockList'])], +}; + +export const ActiveGate = () => ( + {() =>
hello!
}
+); + +export const InactiveGate = () => ( + {() =>
hello!
}
+); diff --git a/src/design-system/atoms/feature-gate/FeatureGate.tsx b/src/design-system/atoms/feature-gate/FeatureGate.tsx new file mode 100644 index 0000000..eb4ba84 --- /dev/null +++ b/src/design-system/atoms/feature-gate/FeatureGate.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { + FeatureFlag, + FeatureFlagsContext, +} from 'roleypoly/src/common/utils/featureFlags/react'; + +export type FeatureGateProps = { + featureFlag: FeatureFlag; + children: () => React.ReactNode; +}; + +export const FeatureGate = (props: FeatureGateProps) => { + const featureContext = React.useContext(FeatureFlagsContext); + + if (featureContext.has(props.featureFlag)) { + return props.children(); + } else { + return <>; + } +}; diff --git a/src/design-system/atoms/feature-gate/index.ts b/src/design-system/atoms/feature-gate/index.ts new file mode 100644 index 0000000..25e59ee --- /dev/null +++ b/src/design-system/atoms/feature-gate/index.ts @@ -0,0 +1 @@ +export * from './FeatureGate'; diff --git a/src/design-system/atoms/horizontal-switch/BUILD.bazel b/src/design-system/atoms/horizontal-switch/BUILD.bazel new file mode 100644 index 0000000..757bea8 --- /dev/null +++ b/src/design-system/atoms/horizontal-switch/BUILD.bazel @@ -0,0 +1,11 @@ +load("//hack/bazel/js:react.bzl", "react_library") + +package(default_visibility = ["//visibility:public"]) + +react_library( + name = "horizontal-switch", + deps = [ + "//src/design-system/atoms/colors", + "//src/design-system/atoms/timings", + ], +) diff --git a/src/design-system/atoms/horizontal-switch/HorizontalSwitch.stories.tsx b/src/design-system/atoms/horizontal-switch/HorizontalSwitch.stories.tsx new file mode 100644 index 0000000..a8a2c45 --- /dev/null +++ b/src/design-system/atoms/horizontal-switch/HorizontalSwitch.stories.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { HorizontalSwitch } from './HorizontalSwitch'; + +export default { + title: 'Atoms/Horizontal Switch', + component: HorizontalSwitch, + args: { + items: ['true', 'false'], + value: 'true', + }, +}; + +const Story = (args) => { + const [value, setValue] = React.useState(args.value); + + return ( + { + setValue(a); + args.onChange(a); + }} + /> + ); +}; + +export const Switch = Story.bind({}); +export const SwitchThree = Story.bind({}); +SwitchThree.args = { + items: ['aaa', 'bbb', 'ccc'], + value: 'aaa', +}; diff --git a/src/design-system/atoms/horizontal-switch/HorizontalSwitch.styled.ts b/src/design-system/atoms/horizontal-switch/HorizontalSwitch.styled.ts new file mode 100644 index 0000000..f94a919 --- /dev/null +++ b/src/design-system/atoms/horizontal-switch/HorizontalSwitch.styled.ts @@ -0,0 +1,25 @@ +import styled, { css } from 'styled-components'; +import * as _ from 'styled-components'; // eslint-disable-line no-duplicate-imports +import { palette } from 'roleypoly/src/design-system/atoms/colors'; +import { transitions } from 'roleypoly/src/design-system/atoms/timings'; + +export const Item = styled.div<{ selected: boolean }>` + padding: 10px; + box-sizing: border-box; + transition: background-color ease-in-out ${transitions.actionable}s; + + ${(props) => + props.selected && + css` + background-color: ${palette.taupe300}; + `} +`; + +export const Wrapper = styled.div` + display: inline-flex; + user-select: none; + cursor: pointer; + border: 1px solid ${palette.taupe200}; + border-radius: calc(1em + 20px); + overflow: hidden; +`; diff --git a/src/design-system/atoms/horizontal-switch/HorizontalSwitch.tsx b/src/design-system/atoms/horizontal-switch/HorizontalSwitch.tsx new file mode 100644 index 0000000..42a4ff9 --- /dev/null +++ b/src/design-system/atoms/horizontal-switch/HorizontalSwitch.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { Item, Wrapper } from './HorizontalSwitch.styled'; + +export type SwitchProps = { + items: string[]; + value: string; + onChange: (value: string) => void; +}; + +export const HorizontalSwitch = (props: SwitchProps) => { + const handleClick = (item: typeof props.value) => () => { + props.onChange?.(item); + }; + + return ( + + {props.items.map((item, idx) => ( + + {item} + + ))} + + ); +}; diff --git a/src/design-system/atoms/horizontal-switch/index.tsx b/src/design-system/atoms/horizontal-switch/index.tsx new file mode 100644 index 0000000..f407392 --- /dev/null +++ b/src/design-system/atoms/horizontal-switch/index.tsx @@ -0,0 +1 @@ +export * from './HorizontalSwitch'; diff --git a/src/design-system/atoms/popover/Popover.tsx b/src/design-system/atoms/popover/Popover.tsx index e01b8bf..1596801 100644 --- a/src/design-system/atoms/popover/Popover.tsx +++ b/src/design-system/atoms/popover/Popover.tsx @@ -10,7 +10,7 @@ import { globalOnKeyUp } from 'roleypoly/src/design-system/atoms/key-events'; import { IoMdClose } from 'react-icons/io'; type PopoverProps = { - children: React.ReactNode; + children: () => React.ReactNode; position: 'top left' | 'top right' | 'bottom left' | 'bottom right'; active: boolean; canDefocus?: boolean; @@ -29,7 +29,7 @@ export const Popover = (props: PopoverProps) => {
{props.headContent}
- {props.children} + {props.children()} {props.canDefocus && ( ` @@ -29,6 +30,9 @@ export const Outer = styled.div` &:hover { transform: translateY(-2px); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); + ${Circle} svg { + fill-opacity: 1; + } } &:active { @@ -69,6 +73,13 @@ export const Circle = styled.div` ? 'var(--role-color)' : 'var(--role-contrast)'}; } + ${(props) => + props.type === 'delete' && + css` + svg { + fill-opacity: 0; + } + `} `; export const Text = styled.div` diff --git a/src/design-system/atoms/role/Role.tsx b/src/design-system/atoms/role/Role.tsx index 411e98d..d0e52b8 100644 --- a/src/design-system/atoms/role/Role.tsx +++ b/src/design-system/atoms/role/Role.tsx @@ -11,6 +11,7 @@ type Props = { disabled?: boolean; onClick?: (newState: boolean) => void; tooltipId?: string; + type?: 'delete'; }; export const Role = (props: Props) => { @@ -36,6 +37,7 @@ export const Role = (props: Props) => { selected: props.selected, defaultColor: props.role.color === 0, disabled: !!props.disabled, + type: props.type, }; const extra = !props.disabled @@ -53,7 +55,7 @@ export const Role = (props: Props) => { {...extra} > - {!props.disabled ? : } + {!props.disabled && props.type !== 'delete' ? : } {props.role.name} diff --git a/src/design-system/molecules/editor-category/EditorCategory.stories.tsx b/src/design-system/molecules/editor-category/EditorCategory.stories.tsx new file mode 100644 index 0000000..e4b1be9 --- /dev/null +++ b/src/design-system/molecules/editor-category/EditorCategory.stories.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { EditorCategory } from './EditorCategory'; +import { + mockCategory, + roleCategory2, + roleCategory, + guildRoles, +} from 'roleypoly/src/design-system/shared-types/storyData'; + +export default { + title: 'Molecules/Editor/Category', +}; + +export const CategoryEditor = () => { + const [categoryData, setCategoryData] = React.useState(mockCategory); + return ( + setCategoryData(category)} + uncategorizedRoles={roleCategory} + guildRoles={[...roleCategory, ...roleCategory2]} + /> + ); +}; diff --git a/src/design-system/molecules/editor-category/EditorCategory.styled.ts b/src/design-system/molecules/editor-category/EditorCategory.styled.ts new file mode 100644 index 0000000..e9a7fb0 --- /dev/null +++ b/src/design-system/molecules/editor-category/EditorCategory.styled.ts @@ -0,0 +1,7 @@ +import * as _ from 'styled-components'; // eslint-disable-line no-duplicate-imports +import styled from 'styled-components'; + +export const RoleContainer = styled.div` + display: flex; + margin: 10px; +`; diff --git a/src/design-system/molecules/editor-category/EditorCategory.tsx b/src/design-system/molecules/editor-category/EditorCategory.tsx new file mode 100644 index 0000000..0132f99 --- /dev/null +++ b/src/design-system/molecules/editor-category/EditorCategory.tsx @@ -0,0 +1,150 @@ +import * as React from 'react'; +import { HorizontalSwitch } from 'roleypoly/src/design-system/atoms/horizontal-switch'; +import { Space } from 'roleypoly/src/design-system/atoms/space'; +import { + TextInput, + TextInputWithIcon, +} from 'roleypoly/src/design-system/atoms/text-input'; +import { Text } from 'roleypoly/src/design-system/atoms/typography'; +import { Popover } from 'roleypoly/src/design-system/atoms/popover'; +import { FaderOpacity } from 'roleypoly/src/design-system/atoms/fader'; +import { RoleSearch } from 'roleypoly/src/design-system/molecules/role-search'; +import { + Category, + CategoryType, + Role as RoleType, +} from 'roleypoly/src/design-system/shared-types'; +import { Role } from 'roleypoly/src/design-system/atoms/role'; +import { GoSearch } from 'react-icons/go'; +import { RoleContainer } from './EditorCategory.styled'; + +type Props = { + category: Category; + uncategorizedRoles: RoleType[]; + guildRoles: RoleType[]; + onChange: (category: Category) => void; +}; + +const typeEnumToSwitch = (typeData: CategoryType) => { + if (typeData === CategoryType.SINGLE) { + return 'Single'; + } else { + return 'Multiple'; + } +}; + +const switchToTypeEnum = (typeData: 'Single' | 'Multiple') => { + if (typeData === 'Single') { + return CategoryType.SINGLE; + } else { + return CategoryType.MULTI; + } +}; + +export const EditorCategory = (props: Props) => { + const [roleSearchPopoverActive, setRoleSearchPopoverActive] = React.useState(false); + const [roleSearchTerm, updateSearchTerm] = React.useState(''); + + const onUpdate = ( + key: keyof typeof props.category, + pred?: (newValue: any) => any + ) => (newValue: any) => { + props.onChange({ + ...props.category, + [key]: pred ? pred(newValue) : newValue, + }); + }; + + const handleRoleSelect = (role: RoleType) => { + setRoleSearchPopoverActive(false); + updateSearchTerm(''); + props.onChange({ + ...props.category, + rolesList: [...props.category.rolesList, role.id], + }); + }; + + const handleRoleDeselect = (role: RoleType) => () => { + props.onChange({ + ...props.category, + rolesList: props.category.rolesList.filter((x) => x !== role.id), + }); + }; + + return ( +
+ Category Name + x.target.value)} + /> + + + + Selection Type +
+ +
+ + + + Visiblity +
+ a === 'Hidden')} + /> +
+ + + Roles + setRoleSearchPopoverActive(false)} + > + {() => ( + updateSearchTerm(newTerm)} + /> + )} + + + } + placeholder={'Type or drag a role...'} + onFocus={() => setRoleSearchPopoverActive(true)} + value={roleSearchTerm} + onChange={(x) => updateSearchTerm(x.target.value)} + /> + + {props.category.rolesList.map((id) => { + const role = props.guildRoles.find((x) => x.id === id); + if (!role) { + return <>; + } + + return ( + + ); + })} + + +
+ ); +}; diff --git a/src/design-system/molecules/editor-category/index.ts b/src/design-system/molecules/editor-category/index.ts new file mode 100644 index 0000000..67c2556 --- /dev/null +++ b/src/design-system/molecules/editor-category/index.ts @@ -0,0 +1 @@ +export * from './EditorCategory'; diff --git a/src/design-system/molecules/help-page-base/BUILD.bazel b/src/design-system/molecules/help-page-base/BUILD.bazel new file mode 100644 index 0000000..f94e5ca --- /dev/null +++ b/src/design-system/molecules/help-page-base/BUILD.bazel @@ -0,0 +1,10 @@ +load("//hack/bazel/js:react.bzl", "react_library") + +package(default_visibility = ["//visibility:public"]) + +react_library( + name = "help-page-base", + deps = [ + "//src/design-system/atoms/colors", + ], +) diff --git a/src/design-system/molecules/role-search/RoleSearch.stories.tsx b/src/design-system/molecules/role-search/RoleSearch.stories.tsx new file mode 100644 index 0000000..b4ec3ee --- /dev/null +++ b/src/design-system/molecules/role-search/RoleSearch.stories.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { RoleSearch } from './RoleSearch'; +import { roleCategory } from 'roleypoly/src/design-system/shared-types/storyData'; + +export default { + title: 'Molecules/Role Search', + component: RoleSearch, + args: { + roles: roleCategory, + searchTerm: '', + }, +}; + +export const Search = (args) => ; diff --git a/src/design-system/molecules/role-search/RoleSearch.tsx b/src/design-system/molecules/role-search/RoleSearch.tsx new file mode 100644 index 0000000..d638ecd --- /dev/null +++ b/src/design-system/molecules/role-search/RoleSearch.tsx @@ -0,0 +1,57 @@ +import styled from 'styled-components'; +import Fuse from 'fuse.js'; +import * as React from 'react'; +import { GoSearch } from 'react-icons/go'; +import { Role } from 'roleypoly/src/design-system/atoms/role'; +import { Space } from 'roleypoly/src/design-system/atoms/space'; +import { TextInputWithIcon } from 'roleypoly/src/design-system/atoms/text-input'; +import { Role as RoleType } from 'roleypoly/src/design-system/shared-types'; + +type Props = { + roles: RoleType[]; + placeholder?: string; + onSelect: (role: RoleType) => void; + onSearchUpdate: (newTerm: string) => void; + searchTerm: string; +}; + +export const RoleSearch = (props: Props) => { + const fuse = new Fuse(props.roles, { includeScore: true, keys: ['name'] }); + const results = + props.searchTerm !== '' + ? fuse.search(props.searchTerm) + : props.roles.map((role) => ({ + item: role, + })); + + const handleClick = (role: RoleType) => () => { + props.onSelect(role); + }; + + return ( +
+ } + placeholder={props.placeholder || 'Search or drag a role...'} + value={props.searchTerm} + onChange={(x) => props.onSearchUpdate(x.target.value)} + /> + + {results.map((resultRole, idx) => ( + + + + ))} +
+ ); +}; + +const RoleInliner = styled.div` + display: flex; + margin: 5px 0; +`; diff --git a/src/design-system/molecules/role-search/index.ts b/src/design-system/molecules/role-search/index.ts new file mode 100644 index 0000000..10fd011 --- /dev/null +++ b/src/design-system/molecules/role-search/index.ts @@ -0,0 +1 @@ +export * from './RoleSearch'; diff --git a/src/design-system/organisms/editor/EditorShell.stories.tsx b/src/design-system/organisms/editor/EditorShell.stories.tsx new file mode 100644 index 0000000..eef53e3 --- /dev/null +++ b/src/design-system/organisms/editor/EditorShell.stories.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import { EditorShell } from './EditorShell'; +import { guildEnum } from 'roleypoly/src/design-system/shared-types/storyData'; + +export default { + title: 'Organisms/Editor', + component: EditorShell, +}; + +export const Shell = () => ; diff --git a/src/design-system/organisms/editor/EditorShell.styled.ts b/src/design-system/organisms/editor/EditorShell.styled.ts new file mode 100644 index 0000000..40f7dae --- /dev/null +++ b/src/design-system/organisms/editor/EditorShell.styled.ts @@ -0,0 +1,8 @@ +import styled from 'styled-components'; +import { palette } from 'roleypoly/src/design-system/atoms/colors'; + +export const CategoryContainer = styled.div` + background-color: ${palette.taupe100}; + padding: 10px; + margin: 15px 0; +`; diff --git a/src/design-system/organisms/editor/EditorShell.tsx b/src/design-system/organisms/editor/EditorShell.tsx new file mode 100644 index 0000000..e186e17 --- /dev/null +++ b/src/design-system/organisms/editor/EditorShell.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { TabView, Tab } from 'roleypoly/src/design-system/atoms/tab-view'; +import { PresentableGuild } from 'roleypoly/src/design-system/shared-types'; +import { EditorCategory } from '../../molecules/editor-category'; +import { CategoryContainer } from './EditorShell.styled'; + +type Props = { + guild: PresentableGuild; +}; + +export const EditorShell = (props: Props) => ( + + {() => } + {() =>
hi2!
}
+
+); + +const RolesTab = (props: Props) => ( +
+ {props.guild.data.categoriesList.map((category, idx) => ( + + console.log(x)} + /> + + ))} +
+); diff --git a/src/design-system/organisms/editor/index.ts b/src/design-system/organisms/editor/index.ts new file mode 100644 index 0000000..80da6a4 --- /dev/null +++ b/src/design-system/organisms/editor/index.ts @@ -0,0 +1 @@ +export * from './EditorShell'; diff --git a/src/design-system/organisms/preauth/BUILD.bazel b/src/design-system/organisms/preauth/BUILD.bazel index 9748b14..23fecaa 100644 --- a/src/design-system/organisms/preauth/BUILD.bazel +++ b/src/design-system/organisms/preauth/BUILD.bazel @@ -3,7 +3,7 @@ load("//hack/bazel/js:react.bzl", "react_library") package(default_visibility = ["//visibility:public"]) react_library( - name = "role-picker", + name = "preauth", deps = [ "react-icons", "//src/design-system/atoms/button", diff --git a/tsconfig.json b/tsconfig.json index e14d668..7453df6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,7 @@ "declaration": true, "moduleResolution": "node", "paths": { - "roleypoly/*": ["./*", "./bazel-bin/*"] + "roleypoly/*": ["./*", "./dist/bin/*"] } } } diff --git a/yarn.lock b/yarn.lock index d37a829..d79c6a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7082,6 +7082,11 @@ fuse.js@^3.6.1: resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.6.1.tgz#7de85fdd6e1b3377c23ce010892656385fd9b10c" integrity sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw== +fuse.js@^6.4.2: + version "6.4.2" + resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.4.2.tgz#bd1400164de6562c077baeaa54ed642b4c2fca62" + integrity sha512-jyy+zOtV96ylMqOGpVjAWQfvEkfTtuJRjsOC6pjReeju8SoDZ2vgFF6eur0a8fxsVwwcpdt3ieaQQeLdkqXH1Q== + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"