diff --git a/common/search.js b/common/search.js
new file mode 100644
index 00000000..03fb8aac
--- /dev/null
+++ b/common/search.js
@@ -0,0 +1,45 @@
+createUserSearchResult = (user) => {
+ return {
+ id: user.id,
+ type: "user",
+ name: user.data.name,
+ username: user.username,
+ url: user.data.photo,
+ };
+};
+
+createSlateSearchResult = (slate) => {
+ let files;
+ if (slate.data.objects.length > 3) {
+ files = slate.data.objects.slice(0, 3);
+ } else {
+ files = slate.data.objects;
+ }
+ return {
+ id: slate.id,
+ type: "slate",
+ name: slate.slatename,
+ username: slate.user.username,
+ url: files.map((file) => {
+ return {
+ type: file.type
+ ? file.type.includes("image")
+ ? "image"
+ : "file"
+ : "file",
+ name: file.name,
+ url: file.url,
+ };
+ }),
+ };
+};
+
+createFileSearchResult = (file) => {
+ return {
+ id: file.id,
+ type: file.type ? (file.type.includes("image") ? "image" : "file") : "file",
+ name: file.name,
+ username: file.user.username,
+ url: file.url,
+ };
+};
diff --git a/common/svg.js b/common/svg.js
index 7caacfbb..f578ccc5 100644
--- a/common/svg.js
+++ b/common/svg.js
@@ -537,3 +537,91 @@ export const Logo = (props) => (
/>
);
+
+export const Slate2 = (props) => (
+
+);
+
+export const Folder2 = (props) => (
+
+);
+
+export const Tool2 = (props) => (
+
+);
+
+export const Wallet2 = (props) => (
+
+);
diff --git a/components/core/Application.js b/components/core/Application.js
index 3e361194..804396f0 100644
--- a/components/core/Application.js
+++ b/components/core/Application.js
@@ -484,6 +484,7 @@ export default class ApplicationPage extends React.Component {
pageTitle={current.target.pageTitle}
currentIndex={this.state.currentIndex}
history={this.state.history}
+ onAction={this._handleAction}
onBack={this._handleBack}
onForward={this._handleForward}
/>
@@ -543,6 +544,7 @@ export default class ApplicationPage extends React.Component {
{scene}
+
);
diff --git a/components/core/ApplicationHeader.js b/components/core/ApplicationHeader.js
index c88976e8..f2c8c423 100644
--- a/components/core/ApplicationHeader.js
+++ b/components/core/ApplicationHeader.js
@@ -3,6 +3,8 @@ import * as Constants from "~/common/constants";
import * as SVG from "~/common/svg";
import { css } from "@emotion/react";
+import { SpotlightSearch } from "~/components/system/modules/SpotlightSearch";
+import { dispatchCustomEvent } from "~/common/custom-events";
const STYLES_ICON_ELEMENT = css`
height: 40px;
@@ -67,6 +69,13 @@ const STYLES_RIGHT = css`
`;
export default class ApplicationHeader extends React.Component {
+ _handleCreateSearch = (e) => {
+ dispatchCustomEvent({
+ name: "create-modal",
+ detail: { modal: },
+ });
+ };
+
render() {
const isBackDisabled =
this.props.currentIndex === 0 || this.props.history.length < 2;
@@ -107,7 +116,7 @@ export default class ApplicationHeader extends React.Component {
window.alert("TODO: SPOTLIGHT SEARCH")}
+ onClick={this._handleCreateSearch}
>
diff --git a/components/system/components/GlobalModal.js b/components/system/components/GlobalModal.js
index 50ed594f..a585de07 100644
--- a/components/system/components/GlobalModal.js
+++ b/components/system/components/GlobalModal.js
@@ -17,27 +17,19 @@ const STYLES_BACKGROUND = css`
display: flex;
align-items: center;
justify-content: center;
- background-color: rgba(45, 41, 38, 0.6);
+ background-color: rgba(0, 0, 0, 0.2);
z-index: ${Constants.zindex.modal};
`;
const STYLES_MODAL = css`
+ box-sizing: border-box;
position: relative;
- padding: 8px;
- max-width: 568px;
+ max-width: 680px;
width: 100%;
- border-radius: 4px;
+ border-radius: 16px;
background-color: ${Constants.system.white};
`;
-const STYLES_CLOSE_ICON = css`
- height: 24px;
- position: absolute;
- top: 8px;
- right: 8px;
- cursor: pointer;
-`;
-
export class GlobalModal extends React.Component {
state = {
modal: null,
@@ -92,11 +84,6 @@ export class GlobalModal extends React.Component {
isDataMenuCaptured={true}
>
-
{this.state.modal}
diff --git a/components/system/components/InputMenu.js b/components/system/components/InputMenu.js
new file mode 100644
index 00000000..aa9c5940
--- /dev/null
+++ b/components/system/components/InputMenu.js
@@ -0,0 +1,201 @@
+import * as React from "react";
+import * as Constants from "~/common/constants";
+import * as SVG from "~/common/svg";
+
+import { css } from "@emotion/react";
+
+const STYLES_DROPDOWN_CONTAINER = css`
+ box-sizing: border-box;
+ z-index: ${Constants.zindex.modal};
+`;
+
+const STYLES_DROPDOWN = css`
+ box-sizing: border-box;
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ background-color: ${Constants.system.white};
+ overflow: hidden;
+ width: 100%;
+`;
+
+const STYLES_DROPDOWN_ITEM = css`
+ box-sizing: border-box;
+ padding: 8px;
+ font-size: 0.8em;
+ border-radius: 16px;
+ border: 1px solid ${Constants.system.white};
+ :hover {
+ border-color: ${Constants.system.border} !important;
+ }
+`;
+
+const STYLES_INPUT = css`
+ font-family: ${Constants.font.text};
+ -webkit-appearance: none;
+ width: 100%;
+ height: 40px;
+ background: ${Constants.system.foreground};
+ color: ${Constants.system.black};
+ display: flex;
+ font-size: 14px;
+ align-items: center;
+ justify-content: flex-start;
+ outline: 0;
+ border: 0;
+ box-sizing: border-box;
+ transition: 200ms ease all;
+ padding: 0 24px 0 48px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ border-radius: 12px;
+ margin-bottom: 16px;
+ ::placeholder {
+ /* Chrome, Firefox, Opera, Safari 10.1+ */
+ color: ${Constants.system.black};
+ opacity: 1; /* Firefox */
+ }
+ :-ms-input-placeholder {
+ /* Internet Explorer 10-11 */
+ color: ${Constants.system.black};
+ }
+ ::-ms-input-placeholder {
+ /* Microsoft Edge */
+ color: ${Constants.system.black};
+ }
+`;
+
+export class InputMenu extends React.Component {
+ _input;
+ _optionRoot;
+
+ state = {
+ selectedIndex: -1,
+ };
+
+ componentDidMount = () => {
+ window.addEventListener("keydown", this._handleDocumentKeydown);
+ this._input.focus();
+ };
+
+ componentWillUnmount = () => {
+ window.removeEventListener("keydown", this._handleDocumentKeydown);
+ };
+
+ _handleInputChange = (e) => {
+ if (this.state.selectedIndex !== -1) {
+ this.setState({ selectedIndex: -1 });
+ }
+ this.props.onChange({
+ target: {
+ value: null,
+ name: this.props.name,
+ },
+ });
+ this.props.onInputChange(e);
+ };
+
+ _handleSelect = (index) => {
+ let e = {
+ target: {
+ value: this.props.options[index].value,
+ name: this.props.name,
+ },
+ };
+ this.props.onChange(e);
+ };
+
+ _handleDocumentKeydown = (e) => {
+ if (e.keyCode === 27) {
+ this._handleDelete();
+ e.preventDefault();
+ } else if (e.keyCode === 9) {
+ this._handleDelete();
+ } else if (e.keyCode === 40) {
+ if (this.state.selectedIndex < this.props.options.length - 1) {
+ let listElem = this._optionRoot.children[this.state.selectedIndex + 1];
+ let elemRect = listElem.getBoundingClientRect();
+ let rootRect = this._optionRoot.getBoundingClientRect();
+ if (elemRect.bottom > rootRect.bottom) {
+ this._optionRoot.scrollTop =
+ listElem.offsetTop +
+ listElem.offsetHeight -
+ this._optionRoot.offsetHeight;
+ }
+ this.setState({ selectedIndex: this.state.selectedIndex + 1 });
+ }
+ e.preventDefault();
+ } else if (e.keyCode === 38) {
+ if (this.state.selectedIndex > 0) {
+ let listElem = this._optionRoot.children[this.state.selectedIndex - 1];
+ let elemRect = listElem.getBoundingClientRect();
+ let rootRect = this._optionRoot.getBoundingClientRect();
+ if (elemRect.top < rootRect.top) {
+ this._optionRoot.scrollTop = listElem.offsetTop;
+ }
+ this.setState({ selectedIndex: this.state.selectedIndex - 1 });
+ }
+ e.preventDefault();
+ } else if (e.keyCode === 13) {
+ if (
+ this.props.options.length > this.state.selectedIndex &&
+ this.state.selectedIndex !== -1
+ ) {
+ this._handleSelect(this.state.selectedIndex);
+ }
+ e.preventDefault();
+ }
+ };
+
+ render() {
+ return (
+
+
+ {
+ this._input = c;
+ }}
+ />
+
+
+ {
+
{
+ this._optionRoot = c;
+ }}
+ css={STYLES_DROPDOWN}
+ style={this.props.style}
+ >
+ {(this.props.options && this.props.options.length
+ ? this.props.options
+ : this.props.defaultOptions
+ ).map((each, i) => (
+
+ {each.name}
+
+ ))}
+
+ }
+
+ );
+ }
+}
diff --git a/components/system/index.js b/components/system/index.js
index a53b9bad..ef1ec289 100644
--- a/components/system/index.js
+++ b/components/system/index.js
@@ -16,6 +16,7 @@ import {
FilecoinRetrievalDealsList,
} from "~/components/system/modules/FilecoinDealsList";
import { FilecoinSettings } from "~/components/system/modules/FilecoinSettings";
+import { SpotlightSearch } from "~/components/system/modules/SpotlightSearch";
// NOTE(jim): Global components
import { GlobalModal } from "~/components/system/components/GlobalModal";
@@ -36,6 +37,7 @@ import { CheckBox } from "~/components/system/components/CheckBox";
import { CodeTextarea } from "~/components/system/components/CodeTextarea";
import { DatePicker } from "~/components/system/components/DatePicker";
import { Input } from "~/components/system/components/Input";
+import { InputMenu } from "~/components/system/components/InputMenu";
import { ListEditor } from "~/components/system/components/ListEditor";
import { HoverTile } from "~/components/system/components/HoverTile";
import { PopoverNavigation } from "~/components/system/components/PopoverNavigation";
@@ -119,6 +121,7 @@ export {
GlobalCarousel,
GlobalNotification,
Input,
+ InputMenu,
HoverTile,
ListEditor,
PopoverNavigation,
@@ -126,6 +129,7 @@ export {
SelectCountryMenu,
SelectMenu,
Slider,
+ SpotlightSearch,
StatUpload,
StatDownload,
TabGroup,
diff --git a/components/system/modules/SpotlightSearch.js b/components/system/modules/SpotlightSearch.js
new file mode 100644
index 00000000..4bb962d6
--- /dev/null
+++ b/components/system/modules/SpotlightSearch.js
@@ -0,0 +1,456 @@
+import * as React from "react";
+import * as Constants from "~/common/constants";
+import * as SVG from "~/common/svg";
+import * as Strings from "~/common/strings";
+
+import MiniSearch from "minisearch";
+import Slate from "~/components/core/Slate";
+
+import { css } from "@emotion/react";
+import { InputMenu } from "~/components/system/components/InputMenu";
+import { dispatchCustomEvent } from "~/common/custom-events";
+
+const fileImg =
+ "https://hub.textile.io/ipfs/bafkreihoi5c3tt4h3qx3gorbi7rrtekgactkpc2tfewwkahxqrxj2elvse";
+
+let items = [
+ {
+ id: "0cc3732d-d572-4ddd-900e-483dd1f4cbfb",
+ type: "user",
+ name: "Haris Butt",
+ username: "haris",
+ url:
+ "https://hub.textile.io/ipfs/bafybeiguo2uhd63reslbqkkgsqedgeikhtuwn5lzqpnqzluoaa3rnkfcvi",
+ },
+ {
+ id: "c32b95ed-9472-4b01-acc2-0fb8303dc140",
+ type: "slate",
+ name: "Doggos",
+ username: "martinalong",
+ url: [
+ {
+ type: "image",
+ name: "tuna.png",
+ url:
+ "https://hub.textile.io/ipfs/bafybeicuz5wrxonu7ud6eskrnshxb66ksg3ncu3ie776xuiydlxrkfuvmu",
+ },
+ {
+ type: "image",
+ name: "khaleesi.jpg",
+ url:
+ "https://hub.textile.io/ipfs/bafkreicb2lookm56omsfjwuwuziwftizmdsj4oneveuqiqlu6k5hc7j5nq",
+ },
+ {
+ type: "file",
+ name:
+ "Seneca - On the Shortness of Life and other things relating to philosophy and culture of the greeks",
+ url:
+ "https://hub.textile.io/ipfs/bafkreic3w24qwy6nxvwzidwvdvmyfeyha5w2uyk6rycli5utdquvafgosq",
+ },
+ ],
+ },
+ {
+ id: "data-75384245-0a6e-4e53-938e-781895556265",
+ type: "image",
+ name: "butter.jpg",
+ username: "jim",
+ url:
+ "https://hub.textile.io/ipfs/bafybeidcn5ucp3mt5bl7vllkeo7uai24ja4ra5i7wctl22ffq2rev7z7au",
+ },
+ {
+ id: "data-bc1bd1c8-5db4-448d-ab35-f4d4866b9fa2",
+ type: "file",
+ name: "seneca-on-the-shortness-of-life.pdf",
+ username: "colin",
+ url:
+ "https://hub.textile.io/ipfs/bafkreic3w24qwy6nxvwzidwvdvmyfeyha5w2uyk6rycli5utdquvafgosq",
+ },
+ {
+ id: "0ba6d7ab-7b1c-4420-bb42-4e66b82df099",
+ type: "slate",
+ name: "Meta",
+ username: "haris",
+ url: [
+ {
+ type: "image",
+ name: "landscape1",
+ url:
+ "https://hub.textile.io/ipfs/bafybeihxn5non5wtt63e2vhk7am4xpmdh3fnmya2vx4jfk52t2jdqudztq",
+ },
+ {
+ type: "image",
+ name: "landscape2",
+ url:
+ "https://hub.textile.io/ipfs/bafybeiddiv44vobree4in7n6gawqzlelpyqwoji6appb6dzpgxzrdonepq",
+ },
+ {
+ type: "image",
+ name: "landscape3",
+ url:
+ "https://hub.textile.io/ipfs/bafkreih2mw66pmi4mvcxb32rhiyas7tohafaiez54lxvy652pdcfmgxrba",
+ },
+ {
+ type: "image",
+ name: "landscape4",
+ url:
+ "https://hub.textile.io/ipfs/bafybeihxn5non5wtt63e2vhk7am4xpmdh3fnmya2vx4jfk52t2jdqudztq",
+ },
+ ],
+ },
+];
+
+const STYLES_ICON_CIRCLE = css`
+ height: 24px;
+ width: 24px;
+ border-radius: 50%;
+ background-color: ${Constants.system.foreground};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`;
+
+const STYLES_MODAL = css`
+ width: 95vw;
+ max-width: 600px;
+ height: 60vh;
+ padding: 24px;
+`;
+
+const STYLES_INPUT_MENU = {
+ height: "calc(100% - 80px)",
+ width: "calc(100% - 48px)",
+ overflowY: "scroll",
+};
+
+const STYLES_USER_ENTRY_CONTAINER = css`
+ display: grid;
+ grid-template-columns: repeat(3, auto) 1fr;
+ grid-column-gap: 16px;
+ align-items: center;
+`;
+
+const STYLES_PROFILE_IMAGE = css`
+ background-size: cover;
+ background-position: 50% 50%;
+ height: 24px;
+ width: 24px;
+ border-radius: 50%;
+`;
+
+const UserEntry = ({ item }) => {
+ //TODO: change from link to onAction once profiles are supported in-client
+ return (
+
+
+
+ );
+};
+
+const STYLES_ENTRY = css`
+ padding: 8px 0px;
+`;
+
+const STYLES_SLATE_ENTRY_CONTAINER = css`
+ display: grid;
+ grid-template-columns: repeat(3, auto) 1fr;
+ grid-column-gap: 16px;
+ align-items: center;
+`;
+
+const STYLES_SLATE_IMAGES_CONTAINER = css`
+ display: grid;
+ grid-template-columns: repeat(3, auto) 1fr;
+ grid-column-gap: 16px;
+ margin: 8px 0px;
+ margin-left: 40px;
+`;
+
+const STYLES_SLATE_IMAGE = css`
+ background-size: cover;
+ background-position: 50% 50%;
+ height: 72px;
+ width: 72px;
+`;
+
+const STYLES_LINK = css`
+ color: ${Constants.system.black};
+ text-decoration: none;
+`;
+
+const STYLES_LINK_HOVER = css`
+ color: ${Constants.system.black};
+ text-decoration: none;
+ :hover {
+ color: ${Constants.system.brand};
+ }
+`;
+
+const STYLES_FILE_ALTERNATE = css`
+ display: flex;
+ justify-content: center;
+ background-color: ${Constants.system.foreground};
+ height: 72px;
+ width: 72px;
+ padding: 4px;
+ text-overflow: ellipsis;
+ word-break: break-word;
+ font-size: 0.7em;
+ overflow: hidden;
+ line-height: 17px;
+`;
+
+const SlateEntry = ({ item, onAction }) => {
+ return (
+ {
+ onAction({ type: "NAVIGATE", value: 17, data: item });
+ }}
+ >
+
+
+
+ {item.url.map((each) =>
+ each.type === "image" ? (
+
+ ) : (
+
{each.name}
+ )
+ )}
+
+
+
+ );
+};
+
+const FileEntry = ({ item, onAction }) => {
+ return (
+ {
+ onAction({ type: "NAVIGATE", value: 15, data: { url: item.url } });
+ }}
+ >
+
+
+ );
+};
+
+const STYLES_DROPDOWN_ITEM = css`
+ display: grid;
+ grid-template-columns: 56px 1fr;
+ align-items: center;
+ cursor: pointer;
+`;
+
+const options = [
+ {
+ name: "Send money",
+ link: null,
+ icon: ,
+ action: { type: "NAVIGATE", value: 2 },
+ },
+ {
+ name: "New slate",
+ link: null,
+ icon: ,
+ action: { type: "NAVIGATE", value: 3 },
+ },
+ {
+ name: "Upload file",
+ link: null,
+ icon: ,
+ action: { type: "NAVIGATE", value: "data" },
+ },
+ {
+ name: "Account settings",
+ link: null,
+ icon: ,
+ action: { type: "NAVIGATE", value: 13 },
+ },
+ {
+ name: "Filecoin settings",
+ link: null,
+ icon: ,
+ action: { type: "NAVIGATE", value: 14 },
+ },
+];
+
+export class SpotlightSearch extends React.Component {
+ state = {
+ options: [],
+ value: null,
+ inputValue: "",
+ };
+
+ componentDidMount = async () => {
+ //let documents = await getDocuments();
+ this.miniSearch = new MiniSearch({
+ fields: ["name", "username"], // fields to index for full-text search
+ storeFields: ["type", "name", "username", "url"], // fields to return with search results
+ searchOptions: {
+ boost: { name: 2 },
+ fuzzy: 0.2,
+ },
+ });
+ //this.miniSearch.addAll(documents);
+ this.miniSearch.addAll(items);
+ };
+
+ _handleChange = (e) => {
+ if (e.target.value !== null) {
+ if (e.target.value.substring(0, 1) === "/") {
+ window.location.pathname = e.target.value;
+ } else {
+ window.location.href = e.target.value;
+ }
+ }
+ };
+
+ _handleInputChange = (e) => {
+ this.setState({ inputValue: e.target.value }, () => {
+ let results = this.miniSearch.search(this.state.inputValue);
+ let options = [];
+ for (let item of results) {
+ if (item.type === "user") {
+ options.push({
+ value: `/${item.username}`,
+ name: ,
+ });
+ } else if (item.type === "slate") {
+ let slug = item.name.toLowerCase().split(" ").join("-");
+ options.push({
+ value: `/${item.username}/${slug}`,
+ name: ,
+ });
+ } else if (item.type === "image" || item.type == "file") {
+ options.push({
+ value: `${item.url}`,
+ name: ,
+ });
+ }
+ }
+ this.setState({ options });
+ });
+ };
+
+ _handleAction = (action) => {
+ this.props.onAction(action);
+ dispatchCustomEvent({
+ name: "delete-modal",
+ detail: {},
+ });
+ };
+
+ render() {
+ return (
+
+
{
+ return {
+ name: (
+ this._handleAction(option.action)}
+ >
+
+ {option.icon}
+
+
{option.name}
+
+ ),
+ value: option.name,
+ };
+ })}
+ />
+
+ );
+ }
+}
+
+const STYLES_ANCHOR_ICON = css`
+ height: 16px;
+ color: ${Constants.system.black};
+`;
+
+const STYLES_ANCHOR_BOX = css`
+ height: 20px;
+ width: 20px;
+ cursor: pointer;
+`;
+
+export class SpotlightSearchAnchor extends React.Component {
+ _handleCreate = (e) => {
+ dispatchCustomEvent({
+ name: "create-modal",
+ detail: { modal: },
+ });
+ };
+
+ render() {
+ return (
+
+
+
+ );
+ }
+}
diff --git a/package.json b/package.json
index 0587d74a..1467cc14 100644
--- a/package.json
+++ b/package.json
@@ -63,6 +63,7 @@
"isomorphic-fetch": "^2.2.1",
"jsonwebtoken": "^8.5.1",
"knex": "^0.20.10",
+ "minisearch": "^2.5.1",
"moment": "^2.27.0",
"next": "^9.4.4",
"pg": "^8.3.0",