diff --git a/src/components/DeviceSearch/DeviceSearch.astro b/src/components/DeviceSearch/DeviceSearch.astro
new file mode 100644
index 0000000..8da111c
--- /dev/null
+++ b/src/components/DeviceSearch/DeviceSearch.astro
@@ -0,0 +1,29 @@
+---
+import "./search.css";
+
+import { DEVICE_MANAGER } from "../../scripts/deviceManager";
+import { getModelItems, getBrandItems } from "../../scripts/algolia/generate";
+const modelItems = getModelItems(DEVICE_MANAGER);
+const brandItems = getBrandItems(DEVICE_MANAGER);
+
+interface Props {
+ id: string;
+}
+
+const { id } = Astro.props;
+---
+
+
+
+<>
+
+
+>
diff --git a/src/styles/search.css b/src/components/DeviceSearch/search.css
similarity index 77%
rename from src/styles/search.css
rename to src/components/DeviceSearch/search.css
index aca5d49..8766a05 100644
--- a/src/styles/search.css
+++ b/src/components/DeviceSearch/search.css
@@ -130,10 +130,47 @@
height: 100%;
}
- /* .aa-InputWrapperPrefix > .aa-DetachedCancelButton {
- height: 100%;
- } */
.aa-InputWrapperSuffix {
width: 48px;
}
}
+
+/* header variant styles */
+
+#device-list__header__autocomplete {
+ max-width: 38%; /* 543 / 1440 */
+ flex: 1 1 0%;
+ margin: 0;
+ height: 56px;
+ background: var(--white);
+}
+
+@media (width >= 992px) {
+ #device-list__header__autocomplete {
+ margin: 0 0 0 144px; /* to prevent search bar overlapping with mockuphone logo */
+ }
+}
+
+@media (width >= 1300px) {
+ #device-list__header__autocomplete {
+ margin: 0;
+ }
+}
+
+.search-device {
+ display: flex;
+ padding: 40px 20px 0;
+}
+
+@media (width >= 992px) {
+ .search-device {
+ display: none;
+ }
+}
+
+#device-list__page__autocomplete {
+ flex: 1 1 0%;
+ margin: 0;
+ height: 56px;
+ background: var(--white);
+}
diff --git a/src/pages/_home.js b/src/components/DeviceSearch/search.js
similarity index 87%
rename from src/pages/_home.js
rename to src/components/DeviceSearch/search.js
index 37b5559..1e04c20 100644
--- a/src/pages/_home.js
+++ b/src/components/DeviceSearch/search.js
@@ -2,39 +2,22 @@ import * as autocompletePluginRecentSearchesPkg from "@algolia/autocomplete-plug
const { createLocalStorageRecentSearchesPlugin } =
autocompletePluginRecentSearchesPkg;
import * as autocompleteJsPkg from "@algolia/autocomplete-js";
-const { autocomplete } = autocompleteJsPkg;
-const NUM_DEFAULT_MODEL_ITEMS_TO_DISPLAY = 0;
-const NUM_DEFAULT_BRAND_ITEMS_TO_DISPLAY = 0;
-const MAX_SEARCH_HISTORY_ITEM = 5;
const ALGOLIA_SEARCH_HISTORY_KEY = "brandModelSearch";
const LOCAL_STORAGE_KEY = `AUTOCOMPLETE_RECENT_SEARCHES:${ALGOLIA_SEARCH_HISTORY_KEY}`;
-class RootViewModel {
- searchText = "";
- _modelItems;
- _brandItems;
-
- constructor(modelItems, brandItems) {
- mobx.makeObservable(this, {
- searchText: mobx.observable,
- shouldShowSearchClear: mobx.computed,
- });
- this._modelItems = modelItems;
- this._brandItems = brandItems;
- }
+const { autocomplete } = autocompleteJsPkg;
- get shouldShowSearchClear() {
- return this.searchText !== "";
- }
-}
+const NUM_DEFAULT_MODEL_ITEMS_TO_DISPLAY = 0;
+const NUM_DEFAULT_BRAND_ITEMS_TO_DISPLAY = 0;
+const MAX_SEARCH_HISTORY_ITEM = 5;
function isArray(obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
}
function appendToLocalStorageRecentSearches(item, type) {
- const existingStr = localStorage.getItem(LOCAL_STORAGE_KEY);
+ const existingStr = localStorage.getItem(LOCAL_STORAGE_KEY) ?? "";
const existing = JSON.parse(existingStr);
const newHistoryItem = { id: item.id, label: item.name, type };
@@ -73,7 +56,15 @@ function moveOldHistoryToTop(oldHistoryItem) {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newHistory));
}
-function initializeAutocomplete(viewModel) {
+function injectVariables() {
+ return {
+ modelItems: window.modelItems,
+ brandItems: window.brandItems,
+ containerIds: window.containerIds,
+ };
+}
+
+function initializeAutocomplete(containerId, modelItems, brandItems) {
const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({
key: ALGOLIA_SEARCH_HISTORY_KEY,
MAX_SEARCH_HISTORY_ITEM,
@@ -81,15 +72,15 @@ function initializeAutocomplete(viewModel) {
return {
...source,
onSelect({ item }) {
- const { id, label } = item;
+ const { id: itemId, label } = item;
const type = item.type ?? "";
moveOldHistoryToTop(item); // move most recent to top
switch (type) {
case "model":
- window.location.href = `/model/${id}`;
+ window.location.href = `/model/${itemId}`;
break;
case "brand":
- window.location.href = `/type/all/?brand=${id}`;
+ window.location.href = `/type/all/?brand=${itemId}`;
break;
default:
window.location.href = `/type/all/?query=${label}`;
@@ -98,12 +89,8 @@ function initializeAutocomplete(viewModel) {
};
},
});
-
- const modelItems = viewModel._modelItems;
- const brandItems = viewModel._brandItems;
-
autocomplete({
- container: "#homepage-autocomplete",
+ container: `#${containerId}`,
openOnFocus: true,
plugins: [recentSearchesPlugin],
placeholder: "Search Device",
@@ -167,6 +154,13 @@ function initializeAutocomplete(viewModel) {
window.location.href = `${window.location.origin}/type/all/?query=${state.query}`;
},
});
+}
+
+function initialize() {
+ const { modelItems, brandItems, containerIds } = injectVariables();
+ containerIds.forEach((containerId) => {
+ initializeAutocomplete(containerId, modelItems, brandItems);
+ });
tippy(".aa-ClearButton", {
content: "Clear",
@@ -208,8 +202,7 @@ function ready(fn) {
}
function main() {
- const viewModel = new RootViewModel(window.modelItems, window.brandItems);
- initializeAutocomplete(viewModel);
+ initialize();
}
ready(main);
diff --git a/src/layouts/BaseLayout/BaseLayout.astro b/src/layouts/BaseLayout/BaseLayout.astro
index 95e35a4..fd9471b 100644
--- a/src/layouts/BaseLayout/BaseLayout.astro
+++ b/src/layouts/BaseLayout/BaseLayout.astro
@@ -2,9 +2,10 @@
import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap/dist/css/bootstrap-grid.min.css";
import "bootstrap/dist/css/bootstrap.min.css";
-import "/src/styles/main.css";
-import "/src/styles/tailwind.css";
+import "./main.css";
+import "./tailwind.css";
import { Image } from "@astrojs/image/components";
+import DeviceSearch from "../../components/DeviceSearch/DeviceSearch.astro";
const title = "MockUPhone";
const description =
@@ -13,6 +14,12 @@ const social_url = "home_url";
const social_icon = `${
import.meta.env.PUBLIC_BASE_URL
}/images/mockuphone-logo.png`;
+
+interface Props {
+ shouldRenderSearchBar?: boolean;
+ isHeaderTransparent?: boolean;
+ mainContainerClass?: string;
+}
const {
shouldRenderSearchBar,
isHeaderTransparent = false,
@@ -134,7 +141,7 @@ const {
{
!!shouldRenderSearchBar ? (
) : undefined
}
diff --git a/src/styles/main.css b/src/layouts/BaseLayout/main.css
similarity index 100%
rename from src/styles/main.css
rename to src/layouts/BaseLayout/main.css
diff --git a/src/styles/tailwind.css b/src/layouts/BaseLayout/tailwind.css
similarity index 100%
rename from src/styles/tailwind.css
rename to src/layouts/BaseLayout/tailwind.css
diff --git a/src/pages/index.astro b/src/pages/index.astro
index c8f0ab8..459c91c 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -3,21 +3,13 @@ import "/src/styles/home.css";
import BaseLayout from "../layouts/BaseLayout/BaseLayout.astro";
import { Image } from "@astrojs/image/components";
-import { DEVICE_MANAGER } from "../scripts/deviceManager";
-import { getModelItems, getBrandItems } from "../scripts/algolia/generate";
-const modelItems = getModelItems(DEVICE_MANAGER);
-const brandItems = getBrandItems(DEVICE_MANAGER);
+import DeviceSearch from "../components/DeviceSearch/DeviceSearch.astro";
---
-
-
@@ -27,7 +19,7 @@ const brandItems = getBrandItems(DEVICE_MANAGER);
Wrap your design in mobile devices in a few clicks!
-
+
-
({
@@ -47,17 +45,12 @@ const brandList: Array = ["all", ...nonEmptyBrands];
content="MockUPhone supports devices including iPhone mockup, iPad mockup, Android mockup and TV mockup. You can check out the whole device list via this page."
/>
-
-
- {/* Add suggestion list here */}
+
diff --git a/src/pages/type/_device.js b/src/pages/type/_device.js
index fe7daab..3af776c 100644
--- a/src/pages/type/_device.js
+++ b/src/pages/type/_device.js
@@ -1,24 +1,10 @@
-import * as autocompletePluginRecentSearchesPkg from "@algolia/autocomplete-plugin-recent-searches";
-const { createLocalStorageRecentSearchesPlugin } =
- autocompletePluginRecentSearchesPkg;
-import * as autocompleteJsPkg from "@algolia/autocomplete-js";
-const { autocomplete } = autocompleteJsPkg;
-
-const NUM_DEFAULT_MODEL_ITEMS_TO_DISPLAY = 0;
-const NUM_DEFAULT_BRAND_ITEMS_TO_DISPLAY = 0;
-const MAX_SEARCH_HISTORY_ITEM = 5;
-const ALGOLIA_SEARCH_HISTORY_KEY = "brandModelSearch";
-const LOCAL_STORAGE_KEY = `AUTOCOMPLETE_RECENT_SEARCHES:${ALGOLIA_SEARCH_HISTORY_KEY}`;
-
class RootViewModel {
submittedQuery = "";
selectedBrand = "all";
_thumbnailList;
_brandThumbnailList;
- _modelItems;
- _brandItems;
- constructor(thumbnailList, brandThumbnailList, modelItems, brandItems) {
+ constructor(thumbnailList, brandThumbnailList) {
mobx.makeObservable(this, {
selectedBrand: mobx.observable,
submittedQuery: mobx.observable,
@@ -26,8 +12,6 @@ class RootViewModel {
});
this._thumbnailList = thumbnailList;
this._brandThumbnailList = brandThumbnailList;
- this._modelItems = modelItems;
- this._brandItems = brandItems;
}
getBrandDeviceQueryResult(brandNodeList, deviceNodeClassName) {
@@ -128,172 +112,6 @@ function handleSelectBrandOption(selectParent, viewModel) {
viewModel.selectedBrand = brand;
}
-function isArray(obj) {
- return Object.prototype.toString.call(obj) === "[object Array]";
-}
-
-function appendToLocalStorageRecentSearches(item, type) {
- const existingStr = localStorage.getItem(LOCAL_STORAGE_KEY);
- const existing = JSON.parse(existingStr);
-
- const newHistoryItem = { id: item.id, label: item.name, type };
-
- if (!isArray(existing)) {
- const newHistory = [newHistoryItem];
- localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newHistory));
- return;
- }
- const hasDuplicateHistory =
- existing.filter((history) => history.id === item.id).length > 0;
- if (hasDuplicateHistory) {
- const existingWithoutDuplicate = existing.filter(
- (history) => history.id !== item.id,
- );
- const newHistory = [newHistoryItem, ...existingWithoutDuplicate];
- localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newHistory));
- return;
- }
-
- const newHistory = [newHistoryItem, ...existing];
- localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newHistory));
-}
-
-function moveOldHistoryToTop(oldHistoryItem) {
- const existingStr = localStorage.getItem(LOCAL_STORAGE_KEY);
- const existing = JSON.parse(existingStr);
- if (!isArray(existing)) {
- // unexpected, just return
- return;
- }
- const existingWithoutDuplicate = existing.filter(
- (history) => history.id !== oldHistoryItem.id,
- );
- const newHistory = [oldHistoryItem, ...existingWithoutDuplicate];
- localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newHistory));
-}
-
-function initializeSearch(viewModel, containerId) {
- const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({
- key: ALGOLIA_SEARCH_HISTORY_KEY,
- MAX_SEARCH_HISTORY_ITEM,
- transformSource({ source }) {
- return {
- ...source,
- onSelect({ item }) {
- const { id, label } = item;
- const type = item.type ?? "";
- moveOldHistoryToTop(item); // move most recent to top
- switch (type) {
- case "model":
- window.location.href = `/model/${id}`;
- break;
- case "brand":
- window.location.href = `/type/all/?brand=${id}`;
- break;
- default:
- window.location.href = `/type/all/?query=${label}`;
- }
- },
- };
- },
- });
- autocomplete({
- container: containerId,
- openOnFocus: true,
- plugins: [recentSearchesPlugin],
- placeholder: "Search Device",
- getSources() {
- return [
- {
- sourceId: "models",
- getItems({ query }) {
- const defaultDisplayItems = modelItems.slice(
- 0,
- NUM_DEFAULT_MODEL_ITEMS_TO_DISPLAY,
- );
- const filtered = modelItems.filter((model) => {
- return model.name.toLowerCase().includes(query.toLowerCase());
- });
- return filtered.length > 0 ? filtered : defaultDisplayItems;
- },
- templates: {
- item({ item, html }) {
- return html`${item.name}`;
- },
- },
- onSelect({ item }) {
- appendToLocalStorageRecentSearches(item, "model");
- },
- getItemUrl({ item }) {
- return `${window.location.origin}${item.pathname}`;
- },
- },
- {
- sourceId: "brands",
- getItems({ query }) {
- const defaultDisplayItems = brandItems.slice(
- 0,
- NUM_DEFAULT_BRAND_ITEMS_TO_DISPLAY,
- );
- const filtered = brandItems.filter((brand) => {
- return brand.name.toLowerCase().includes(query.toLowerCase());
- });
- return filtered.length > 0 ? filtered : defaultDisplayItems;
- },
- templates: {
- item({ item, components, html }) {
- return html`${item.name}`;
- },
- },
- onSelect({ item }) {
- appendToLocalStorageRecentSearches(item, "brand");
- },
- getItemUrl({ item }) {
- return `${window.location.origin}${item.pathname}`;
- },
- },
- ];
- },
- onSubmit({ state }) {
- window.location.href = `${window.location.origin}/type/all/?query=${state.query}`;
- },
- });
-
- tippy(".aa-ClearButton", {
- content: "Clear",
- placement: "bottom",
- theme: "light-border",
- });
-
- const detachedSearchButtonList = document.querySelectorAll(
- ".aa-DetachedSearchButton",
- );
- detachedSearchButtonList.forEach((detachedSearchButton) => {
- detachedSearchButton.addEventListener("click", () => {
- const inputForm = document.querySelector(".aa-Form");
-
- if (inputForm.querySelector(".aa-DetachedCancelButton") != null) {
- return;
- }
- const newCancelButton = document.createElement("button");
- newCancelButton.type = "button";
- newCancelButton.classList.add("aa-DetachedCancelButton");
-
- newCancelButton.addEventListener("click", () => {
- // ref https://github.com/algolia/autocomplete/blob/d0b3b27d2d22f06590cef5606062ca0e48c9003f/packages/autocomplete-js/src/__tests__/detached.test.ts#L420
- const detachedContainer = document.querySelector(".aa-DetachedOverlay");
- document.body.removeChild(detachedContainer);
- document.body.classList.remove("aa-Detached");
- });
- inputForm.appendChild(newCancelButton);
- });
- });
-}
-
function handleBrandSearchParams(viewModel) {
const urlParams = new URLSearchParams(window.location.search);
const brandParam = urlParams.get("brand");
@@ -375,17 +193,8 @@ function main() {
const viewModel = new RootViewModel(
window.thumbnailList,
window.brandThumbnailList,
- window.modelItems,
- window.brandItems,
);
- [
- "#device-list__header__autocomplete",
- "#device-list__page__autocomplete",
- ].forEach((containerId) => {
- initializeSearch(viewModel, containerId);
- });
-
handleSubmittedQueryChange(viewModel);
mobx.reaction(
diff --git a/src/pages/type/index.css b/src/pages/type/index.css
index dca7d58..71e196e 100644
--- a/src/pages/type/index.css
+++ b/src/pages/type/index.css
@@ -1,5 +1,3 @@
-@import url("../../styles/search.css");
-
.header__search-container {
display: none;
}
@@ -15,44 +13,6 @@
}
}
-#device-list__header__autocomplete {
- max-width: 38%; /* 543 / 1440 */
- flex: 1 1 0%;
- margin: 0;
- height: 56px;
- background: var(--white);
-}
-
-@media (width >= 992px) {
- #device-list__header__autocomplete {
- margin: 0 0 0 144px; /* to prevent search bar overlapping with mockuphone logo */
- }
-}
-
-@media (width >= 1300px) {
- #device-list__header__autocomplete {
- margin: 0;
- }
-}
-
-.search-device {
- display: flex;
- padding: 40px 20px 0;
-}
-
-@media (width >= 992px) {
- .search-device {
- display: none;
- }
-}
-
-#device-list__page__autocomplete {
- flex: 1 1 0%;
- margin: 0;
- height: 56px;
- background: var(--white);
-}
-
.device-type {
padding: 40px 0;
}
diff --git a/src/scripts/algolia/generate.ts b/src/scripts/algolia/generate.ts
index 2a7849b..eaef09d 100644
--- a/src/scripts/algolia/generate.ts
+++ b/src/scripts/algolia/generate.ts
@@ -2,7 +2,21 @@ import { DeviceManager } from "../deviceManager";
import { BrandValue, ModelValue } from "../model";
import { BrandEnum, ModelEnum } from "../parse";
-export const getModelItems = (deviceManager: DeviceManager) =>
+export interface SearchBarModelItem {
+ id: ModelEnum;
+ name: string;
+ pathname: string;
+}
+
+export interface SearchBarBrandItem {
+ id: BrandEnum;
+ name: string;
+ pathname: string;
+}
+
+export const getModelItems = (
+ deviceManager: DeviceManager,
+): SearchBarModelItem[] =>
Object.keys(deviceManager.allDeviceModels)
.map((modelKey) => {
const _modelKey: ModelEnum = ModelEnum.parse(modelKey);
@@ -20,7 +34,9 @@ export const getModelItems = (deviceManager: DeviceManager) =>
};
});
-export const getBrandItems = (deviceManager: DeviceManager) =>
+export const getBrandItems = (
+ deviceManager: DeviceManager,
+): SearchBarBrandItem[] =>
Object.keys(deviceManager.allBrands)
.map((brandKey) => {
const _brandKey: BrandEnum = BrandEnum.parse(brandKey);
diff --git a/src/styles/home.css b/src/styles/home.css
index b277770..455dd9c 100644
--- a/src/styles/home.css
+++ b/src/styles/home.css
@@ -1,5 +1,3 @@
-@import url("./search.css");
-
.main--landing-content {
display: flex;
flex-direction: row;