diff --git a/src/AppStore/AppThumbnail.jsx b/src/AppStore/AppThumbnail.jsx
new file mode 100644
index 00000000..4717cc1a
--- /dev/null
+++ b/src/AppStore/AppThumbnail.jsx
@@ -0,0 +1,91 @@
+const appDetailsUrl = `#/${REPL_ACCOUNT}/widget/ComponentDetailsPage?src=${props.author}/widget/${props.widgetName}`;
+
+const Thumbnail = styled.a`
+ display: block;
+ aspect-ratio: 1 / 1;
+ overflow: hidden;
+ border-radius: 1.25rem;
+ border: 1px solid var(--sand6);
+ position: relative;
+ cursor: pointer;
+ text-decoration: none !important;
+ outline: none;
+ transition: all 200ms;
+
+ img {
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border: none;
+ }
+
+ &:hover,
+ &:focus {
+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.15);
+ }
+`;
+
+const ThumbnailContent = styled.span`
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 1.25rem;
+ padding-top: 3.5rem;
+ background: linear-gradient(
+ to top,
+ rgba(0, 0, 0, 0.85) 20%,
+ rgba(0, 0, 0, 0)
+ );
+ font: var(--text-xs);
+ color: var(--white);
+ text-shadow: 0 0 2px rgba(0, 0, 0, 0.75);
+
+ b {
+ font-weight: 600;
+ }
+
+ @media (max-width: 650px) {
+ padding: 0.75rem;
+ }
+`;
+
+const ThumbnailTag = styled.span`
+ display: inline-flex;
+ border-bottom-right-radius: 1.25rem;
+ background: var(--violet7);
+ color: #fff;
+ font: var(--text-xs);
+ font-weight: 700;
+ padding: 0.25rem 0.75rem;
+ position: absolute;
+ top: 0;
+ left: 0;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+ border-right: 1px solid rgba(0, 0, 0, 0.1);
+`;
+
+return (
+
+
+
+
+
+ {props.name ?? props.widgetName}
+
+ {props.author}
+
+
+);
diff --git a/src/AppStore/ArticleSummary.jsx b/src/AppStore/ArticleSummary.jsx
new file mode 100644
index 00000000..9cdeba14
--- /dev/null
+++ b/src/AppStore/ArticleSummary.jsx
@@ -0,0 +1,68 @@
+const Text = styled.p`
+ font: var(--${(p) => p.size ?? "text-base"});
+ font-weight: ${(p) => p.fontWeight};
+ color: var(--${(p) => p.color ?? "sand12"});
+ margin: 0;
+`;
+
+const ArticleImage = styled.div`
+ width: 100%;
+ aspect-ratio: 26 / 15;
+ border-radius: 1rem;
+ border: 1px solid var(--sand6);
+ overflow: hidden;
+ transition: all 200ms;
+
+ img {
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border: none;
+ }
+`;
+
+const ArticleContent = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+`;
+
+const Article = styled.a`
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ align-items: row;
+ cursor: pointer;
+ text-decoration: none !important;
+
+ &:hover,
+ &:focus {
+ ${ArticleImage} {
+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.15);
+ }
+ }
+`;
+
+return (
+
+
+
+
+
+
+
+ {props.title}
+
+ {props.author}
+
+
+);
diff --git a/src/AppStore/IndexPage.jsx b/src/AppStore/IndexPage.jsx
new file mode 100644
index 00000000..b10098b1
--- /dev/null
+++ b/src/AppStore/IndexPage.jsx
@@ -0,0 +1,301 @@
+State.init({
+ categories: [],
+ isLoading: true,
+ selectedTab: props.tab,
+});
+
+if (props.tab && props.tab !== state.selectedTab) {
+ State.update({
+ selectedTab: props.tab,
+ });
+}
+
+const appStoreIndexUrl = "#/${REPL_ACCOUNT}/widget/AppStore.IndexPage";
+const selectedCategory = state.categories.find(
+ (category) => category.label === state.selectedTab
+);
+
+function loadData() {
+ if (state.categories.length > 0) return;
+
+ asyncFetch(
+ "https://storage.googleapis.com/databricks-near-query-runner/output/app-store.json"
+ )
+ .then((res) => {
+ State.update({
+ categories: res.body.categories,
+ isLoading: false,
+ selectedTab: state.selectedTab ?? res.body.categories[0].label,
+ });
+ })
+ .catch((error) => {
+ State.update({
+ isLoading: false,
+ });
+ console.log(error);
+ });
+}
+
+loadData();
+
+const Wrapper = styled.div`
+ padding: 100px 0;
+ background: url("https://ipfs.near.social/ipfs/bafkreie5t75jirebnuyozmsc5hxzhxpoqivaxmc4rypaaogab6qh7asb2i");
+ background-position: right top;
+ background-size: 1440px auto;
+ background-repeat: no-repeat;
+ margin-top: calc(var(--body-top-padding) * -1);
+
+ @media (max-width: 1024px) {
+ padding: 50px 0;
+ }
+
+ @media (max-width: 800px) {
+ background-image: none;
+ padding: 2rem 0;
+ }
+`;
+
+const Container = styled.div`
+ max-width: 1120px;
+ margin: 0 auto;
+ padding: 0 16px;
+`;
+
+const Main = styled.div`
+ display: flex;
+ gap: 6.5rem;
+
+ @media (max-width: 1024px) {
+ flex-direction: column;
+ gap: 3rem;
+ }
+
+ @media (max-width: 800px) {
+ gap: 2rem;
+ }
+`;
+
+const Menu = styled.div`
+ width: 7.5rem;
+ flex-shrink: 0;
+ text-align: right;
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+ overflow: auto;
+ scroll-behavior: smooth;
+
+ @media (max-width: 1024px) {
+ width: 100%;
+ text-align: left;
+ flex-direction: row;
+ }
+`;
+
+const MenuLink = styled.a`
+ display: block;
+ font: var(--text-s);
+ font-weight: 600;
+ color: var(--sand12);
+ outline: none;
+
+ &:hover,
+ &:focus {
+ color: var(--sand12);
+ text-decoration: underline;
+ }
+
+ &[data-active="true"] {
+ color: var(--violet7);
+ }
+`;
+
+const Sections = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 3rem;
+ flex-grow: 1;
+`;
+
+const Section = styled.div``;
+
+const H1 = styled.h1`
+ font: var(--text-hero);
+ color: var(--sand12);
+ margin: 0 0 3rem;
+ padding-left: 14rem;
+
+ @media (max-width: 1024px) {
+ padding-left: 0;
+ }
+
+ @media (max-width: 800px) {
+ font: var(--text-3xl);
+ font-weight: 600;
+ margin: 0 0 2rem;
+ }
+`;
+
+const H2 = styled.h2`
+ font: var(--text-l);
+ color: var(--sand12);
+ margin: 0 0 1.5rem;
+ font-weight: 600;
+`;
+
+const Text = styled.p`
+ font: var(--${(p) => p.size ?? "text-base"});
+ font-weight: ${(p) => p.fontWeight};
+ color: var(--${(p) => p.color ?? "sand12"});
+ margin: 0;
+`;
+
+const ThumbnailGrid = styled.div`
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr 1fr;
+ gap: 1.5rem;
+
+ @media (max-width: 850px) {
+ grid-template-columns: 1fr 1fr 1fr;
+ }
+
+ @media (max-width: 550px) {
+ gap: 0.5rem;
+ grid-template-columns: 1fr 1fr;
+ }
+`;
+
+const ContentGrid = styled.div`
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 2rem;
+
+ @media (max-width: 650px) {
+ grid-template-columns: 1fr;
+ }
+`;
+
+return (
+
+
+
+ {state.selectedTab === "Search" ? "Search" : selectedCategory?.title}
+
+
+
+
+
+
+ {state.selectedTab === "Search" && (
+
+ )}
+
+ {state.selectedTab !== "Search" && selectedCategory && (
+ <>
+ {selectedCategory.sections.map((section) => {
+ switch (section.format) {
+ case "MEDIUM":
+ return (
+
+ {section.title && {section.title}
}
+
+ {section.items.map((item) => {
+ return (
+
+ );
+ })}
+
+
+ );
+
+ case "SMALL":
+ return (
+
+ {section.title && {section.title}
}
+
+
+ {section.items.map((item) => {
+ return (
+
+ );
+ })}
+
+
+ );
+
+ case "ARTICLE":
+ return (
+
+ {section.title && {section.title}
}
+
+
+ {section.items.map((item) => {
+ return (
+
+ );
+ })}
+
+
+ );
+
+ default:
+ return null;
+ }
+ })}
+ >
+ )}
+
+
+
+
+);
diff --git a/src/AppStore/Search.jsx b/src/AppStore/Search.jsx
new file mode 100644
index 00000000..a9b24847
--- /dev/null
+++ b/src/AppStore/Search.jsx
@@ -0,0 +1,156 @@
+function search(query) {
+ if (!query) {
+ State.update({
+ isLoading: false,
+ results: [],
+ });
+ return;
+ }
+
+ const body = {
+ query,
+ page: state.currentPage,
+ filters: "categories:widget AND tags:app AND NOT _tags:hidden",
+ };
+
+ asyncFetch("/api/search", {
+ body: JSON.stringify(body),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ method: "POST",
+ })
+ .then((res) => {
+ State.update({
+ isLoading: false,
+ results: [...state.results, ...res.body.hits],
+ totalPages: res.body.nbPages,
+ });
+ })
+ .catch((error) => {
+ State.update({
+ isLoading: false,
+ });
+ console.log(error);
+ });
+}
+
+function handleOnInput() {
+ State.update({
+ currentPage: 0,
+ totalPages: 0,
+ isLoading: true,
+ results: [],
+ });
+}
+
+function handleOnQueryChange(query) {
+ State.update({
+ query,
+ });
+ search(query);
+}
+
+function loadMore() {
+ State.update({
+ currentPage: state.currentPage + 1,
+ isLoading: true,
+ });
+
+ search(state.query);
+}
+
+State.init({
+ currentPage: 0,
+ totalPages: 0,
+ isLoading: false,
+ query: "",
+ results: [],
+});
+
+const Wrapper = styled.div`
+ display: flex;
+ width: 100%;
+ flex-direction: column;
+ gap: 3rem;
+`;
+
+const H2 = styled.h2`
+ font: var(--text-l);
+ color: var(--sand12);
+ margin: 0;
+ font-weight: 600;
+`;
+
+const Text = styled.p`
+ font: var(--${(p) => p.size ?? "text-base"});
+ font-weight: ${(p) => p.fontWeight};
+ color: var(--${(p) => p.color ?? "sand12"});
+ margin: 0;
+`;
+
+const ContentGrid = styled.div`
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 2rem;
+
+ @media (max-width: 650px) {
+ grid-template-columns: 1fr;
+ }
+`;
+
+return (
+
+
+
+ {state.query && state.results.length === 0 && !state.isLoading && (
+ No apps matched your search: "{state.query}"
+ )}
+
+ {state.results.length > 0 && (
+ <>
+ All apps
+
+
+ {state.results.map((result) => {
+ return (
+
+ );
+ })}
+
+ >
+ )}
+
+ {state.currentPage + 1 < state.totalPages && (
+
+ )}
+
+);
diff --git a/src/ComponentCard.jsx b/src/ComponentCard.jsx
index 6b1ae9a7..b17d85c5 100644
--- a/src/ComponentCard.jsx
+++ b/src/ComponentCard.jsx
@@ -1,12 +1,14 @@
-const [accountId, widget, widgetName] = props.src.split("/");
-const metadata = Social.get(
- `${accountId}/widget/${widgetName}/metadata/**`,
- "final"
-);
-const tags = Object.keys(metadata.tags || {});
+const [accountId, unused, widgetName] = props.src.split("/");
const detailsUrl = `#/${REPL_ACCOUNT}/widget/ComponentDetailsPage?src=${accountId}/widget/${widgetName}`;
const appUrl = `#/${accountId}/widget/${widgetName}`;
const accountUrl = `#/${REPL_ACCOUNT}/widget/ProfilePage?accountId=${accountId}`;
+const metadata =
+ props.metadata ??
+ Social.get(`${accountId}/widget/${widgetName}/metadata/**`, "final") ??
+ {};
+const tags = props.metadata
+ ? props.metadata.tags
+ : Object.keys(metadata.tags || {});
const Card = styled.div`
position: relative;
diff --git a/src/ComponentDetailsPage.jsx b/src/ComponentDetailsPage.jsx
index 46ac5114..543472b2 100644
--- a/src/ComponentDetailsPage.jsx
+++ b/src/ComponentDetailsPage.jsx
@@ -1,6 +1,6 @@
State.init({
copiedShareUrl: false,
- selectedTab: props.tab ?? "source",
+ selectedTab: props.tab ?? "about",
});
if (props.tab && props.tab !== state.selectedTab) {
diff --git a/src/DIG/Accordion.jsx b/src/DIG/Accordion.jsx
index 6cab8232..87ab0f41 100644
--- a/src/DIG/Accordion.jsx
+++ b/src/DIG/Accordion.jsx
@@ -15,7 +15,7 @@ const Item = styled("Accordion.Item")`
`;
const Header = styled("Accordion.Header")`
- margin: 0;
+ margin: 0 !important;
`;
const Trigger = styled("Accordion.Trigger")`
diff --git a/src/DIG/Button.metadata.json b/src/DIG/Button.metadata.json
index 5a6efebf..a9c71154 100644
--- a/src/DIG/Button.metadata.json
+++ b/src/DIG/Button.metadata.json
@@ -1,7 +1,7 @@
{
- "description": "A fully featured button component that can act as a `