diff --git a/workspaces/cms-config/src/collections/categories.ts b/workspaces/cms-config/src/collections/categories.ts
index 896f0ff7afe..cedd023a6c9 100644
--- a/workspaces/cms-config/src/collections/categories.ts
+++ b/workspaces/cms-config/src/collections/categories.ts
@@ -24,6 +24,16 @@ export const categoriesCollectionConfig = {
widget: "string",
crowdin: true
},
+ {
+ name: 'parentCategory',
+ label: 'Parent category',
+ widget: 'relation',
+ collection: 'categories',
+ search_fields: ['name'],
+ value_field: 'id',
+ display_fields: ['name'],
+ options_length: 300
+ },
{
name: "show_custom_featured_post",
label: "Show custom featured post",
diff --git a/workspaces/cms-config/src/main.ts b/workspaces/cms-config/src/main.ts
index d6667ab60b9..027a6c6a9fb 100644
--- a/workspaces/cms-config/src/main.ts
+++ b/workspaces/cms-config/src/main.ts
@@ -14,7 +14,9 @@ export const CMSConfig = {
branch: "dev",
base_url: "https://netlify-cms-auth.haim-6b2.workers.dev",
preview_context: "Vercel – starknet-website",
+ local_backend: true,
},
+ local_backend: true,
publish_mode: "editorial_workflow",
show_preview_links: true,
media_folder: "public/assets",
diff --git a/workspaces/cms-data/src/categories.ts b/workspaces/cms-data/src/categories.ts
index 1538c9abf7c..ea140a3a818 100644
--- a/workspaces/cms-data/src/categories.ts
+++ b/workspaces/cms-data/src/categories.ts
@@ -4,6 +4,7 @@ import { getFirst, getJSON } from "@starknet-io/cms-utils/src/index";
export interface Category {
readonly id: string;
readonly slug: string;
+ readonly parentCategory?: string;
readonly name: string;
readonly show_custom_featured_post?: boolean;
readonly custom_featured_post?: string;
diff --git a/workspaces/website/src/components/ArticleCard/ArticleCard.tsx b/workspaces/website/src/components/ArticleCard/ArticleCard.tsx
index f8efcb9691e..e79e0e7385e 100644
--- a/workspaces/website/src/components/ArticleCard/ArticleCard.tsx
+++ b/workspaces/website/src/components/ArticleCard/ArticleCard.tsx
@@ -7,13 +7,16 @@ import {
Icon,
Flex,
ChakraProps,
- useBreakpointValue
+ useBreakpointValue,
+ BoxProps,
+ FlexProps
} from "@chakra-ui/react";
import { Text } from "@ui/Typography/Text";
import { Heading } from "@ui/Typography/Heading";
-import { FiBookOpen, FiHeadphones, FiTv } from "react-icons/fi";
import { CardGradientBorder } from "@ui/Card/components/CardGradientBorder";
import { Category as DataCategory } from "@starknet-io/cms-data/src/categories";
+import { ReactNode } from "react";
+import { FiBookOpen, FiHeadphones, FiTv } from "react-icons/fi";
type RootProps = {
children: React.ReactNode;
@@ -43,39 +46,53 @@ const Root = ({ children, href, type = "grid", sx }: RootProps) => {
);
};
-type ImageProps = {
+type ImageProps = BoxProps & {
+ children?: ReactNode;
url?: string;
imageAlt?: string;
type?: | "grid" | "featured";
};
-const Image = ({ url, imageAlt, type = "grid" }: ImageProps) => {
+const Image = ({ children, url, imageAlt, type = "grid", ...rest }: ImageProps) => {
const size = useBreakpointValue({ base: '581px', sm: '350px', md: '430px', xl: '320px' });
const featuredImageSize = useBreakpointValue({ base: '581px', sm: '350px', md: '430px', lg: '550px', xl: '606px' });
const cloudflareImage = `https://starknet.io/cdn-cgi/image/width=${type === "featured" ? featuredImageSize : size},height=auto,format=auto${url}`;
const isProd = import.meta.env.VITE_ALGOLIA_INDEX === "production";
return (
-
+
+
+ {children}
);
};
-type BodyProps = {
+type BodyProps = FlexProps & {
children: React.ReactNode;
type?: | "grid" | "featured";
};
-const Body = ({ children, type = "grid" }: BodyProps) => {
+const Body = ({ children, type = "grid", ...rest }: BodyProps) => {
return (
-
+
{children}
);
@@ -95,15 +112,19 @@ const Category = ({ category }: CategoryProps) => {
);
};
-type ContentProps = {
+type ContentProps = FlexProps & {
title: string;
excerpt: string;
type?: | "grid" | "featured";
};
-const Content = ({ title, excerpt, type = "grid" }: ContentProps) => {
+const Content = ({ title, excerpt, type = "grid", ...rest }: ContentProps) => {
return (
-
+
{
>
{title}
-
+
{excerpt}
);
};
-type FooterProps = {
+type FooterProps = FlexProps & {
+ hideIcon?: boolean;
postType: string;
publishedAt?: string;
timeToConsume?: string;
type?: | "grid" | "featured";
};
const Footer = ({
+ hideIcon,
postType,
publishedAt = "N/A",
timeToConsume = "5min read",
- type = "grid"
+ type = "grid",
+ ...rest
}: FooterProps) => {
const renderPostTypeIcon = () => {
switch (postType) {
@@ -146,9 +170,14 @@ const Footer = ({
}
};
return (
-
+
+ {!hideIcon && (
+ )}
{publishedAt} ·
diff --git a/workspaces/website/src/components/Blog/BlogCard.tsx b/workspaces/website/src/components/Blog/BlogCard.tsx
new file mode 100644
index 00000000000..19044a066ab
--- /dev/null
+++ b/workspaces/website/src/components/Blog/BlogCard.tsx
@@ -0,0 +1,130 @@
+/**
+ * Module dependencies
+ */
+
+import { BlogHit } from "src/pages/posts/CategoryPage";
+import {
+ Body,
+ Content,
+ Footer,
+ Image,
+ Root
+} from "@ui/ArticleCard/ArticleCard";
+
+import moment from "moment";
+import { Topic } from "@starknet-io/cms-data/src/topics";
+import { Tag } from "@chakra-ui/tag";
+import { useMemo } from "react";
+import { Box, Flex, Grid, Icon } from "@chakra-ui/react";
+import { IoPlaySharp } from "react-icons/io5";
+
+/**
+ * `Props` type.
+ */
+
+type Props = {
+ isFeatured?: boolean;
+ post: BlogHit;
+ topics: Topic[]
+};
+
+
+/**
+ * Export `BlogCard` component.
+ */
+
+export const BlogCard = ({ isFeatured, post, topics }: Props) => {
+ const topicNames = useMemo(() => (
+ post.topic
+ .slice(0, 3)
+ .map((topic) => topics.find(({ id }) => id === topic)?.name)
+ .filter((topic) => !!topic)
+ ), [post, topics])
+
+ return (
+
+
+ {(post.post_type === 'video' || post.post_type == 'audio') && (
+
+
+
+ )}
+
+
+
+ {topicNames.length > 0 && (
+
+ {topicNames.length !== 0 && (
+
+ {topicNames.map((topic) => (
+
+ {topic}
+
+ ))}
+
+ )}
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/workspaces/website/src/components/Blog/BlogSection.tsx b/workspaces/website/src/components/Blog/BlogSection.tsx
new file mode 100644
index 00000000000..f5ac80421e3
--- /dev/null
+++ b/workspaces/website/src/components/Blog/BlogSection.tsx
@@ -0,0 +1,93 @@
+/**
+ * Module dependencies
+ */
+
+import { Box, Grid, Icon } from "@chakra-ui/react";
+import { IoArrowForward } from "react-icons/io5";
+import { Button } from "@ui/Button";
+import { Heading } from "@ui/Typography/Heading";
+import { useHits } from "react-instantsearch-hooks-web";
+import { BlogHit } from "src/pages/posts/CategoryPage";
+import { BlogCard } from "./BlogCard";
+import { Topic } from "@starknet-io/cms-data/src/topics";
+import { useMemo } from "react";
+import { EmptySection } from "./EmptySection";
+
+
+/**
+ * `Props` type.
+ */
+
+export type Props = {
+ title: string;
+ topics: Topic[];
+ url: string;
+};
+
+/**
+ * Export `BlogSection` component.
+ */
+
+export const BlogSection = (props: Props) => {
+ const { title, topics, url } = props;
+ const { hits } = useHits();
+ const posts = useMemo(() => {
+ const sectionSize = hits.some(({ post_type }) => post_type !== 'video' && post_type !== 'audio') ? 3 : 2
+ const paddedHits = hits.concat(Array(sectionSize).fill(undefined))
+ return paddedHits.slice(0, sectionSize)
+ }, [hits])
+
+ return (
+
+
+ {title}
+
+
+
+
+ {(hits ?? []).length > 0 ? (
+
+ {posts.map(hit => {
+ if(hit) {
+ return (
+
+ )
+ }
+
+ return
+ })}
+
+ ): (
+
+ )}
+
+ );
+}
diff --git a/workspaces/website/src/components/Blog/CategoryList.tsx b/workspaces/website/src/components/Blog/CategoryList.tsx
new file mode 100644
index 00000000000..f6d39bb7e42
--- /dev/null
+++ b/workspaces/website/src/components/Blog/CategoryList.tsx
@@ -0,0 +1,80 @@
+/**
+ * Module dependencies
+ */
+
+import { Box, Flex, Heading } from "@chakra-ui/react";
+import { Category } from "@starknet-io/cms-data/src/categories";
+import { Button } from "@ui/Button";
+import { useMemo } from "react";
+import { normalizeCategories } from "src/utils/blog";
+import { navigate } from "vite-plugin-ssr/client/router";
+
+
+/**
+ * `Props` type.
+ */
+
+export type Props = {
+ categories: readonly Category[];
+ params: LocaleParams & {
+ readonly category?: string;
+ };
+};
+
+/**
+ * Export `CategoryList` component.
+ */
+
+export const CategoryList = ({ categories, params }: Props) => {
+ const normalizedCategories = useMemo(() => (
+ normalizeCategories(categories).filter((category) => !category.parentCategory)
+ ), [categories]);
+
+ return (
+
+
+ {'Explore'}
+
+
+
+
+
+
+ {normalizedCategories.map((category, index) => (
+
+
+
+ ))}
+
+ );
+}
diff --git a/workspaces/website/src/components/Blog/EmptySection.tsx b/workspaces/website/src/components/Blog/EmptySection.tsx
new file mode 100644
index 00000000000..ac72323373f
--- /dev/null
+++ b/workspaces/website/src/components/Blog/EmptySection.tsx
@@ -0,0 +1,27 @@
+/**
+ * Module dependencies
+ */
+
+import { Divider, Grid } from "@chakra-ui/react";
+import { Text } from "@ui/Typography/Text";
+
+/**
+ * Export `EmptySection` component.
+ */
+
+export const EmptySection = () => (
+
+
+
+
+ {'No content found'}
+
+
+
+
+);
diff --git a/workspaces/website/src/components/Blog/FeaturedSection.tsx b/workspaces/website/src/components/Blog/FeaturedSection.tsx
new file mode 100644
index 00000000000..17a4c1d23c9
--- /dev/null
+++ b/workspaces/website/src/components/Blog/FeaturedSection.tsx
@@ -0,0 +1,210 @@
+/**
+ * Module dependencies
+ */
+
+import { Box, Flex, Grid } from "@chakra-ui/react";
+import { Heading } from "@ui/Typography/Heading";
+import { Configure, useHits } from "react-instantsearch-hooks-web";
+import { BlogHit } from "src/pages/posts/CategoryPage";
+import { BlogCard } from "./BlogCard";
+import { Topic } from "@starknet-io/cms-data/src/topics";
+import { IconButton } from "@ui/IconButton";
+import { IoArrowBack, IoArrowForward } from "react-icons/io5";
+import { useCallback, useEffect, useState } from "react";
+
+/**
+ * `featuredPostsPadding` constant.
+ */
+
+const featuredPostsPadding = 984;
+
+/**
+ * `Props` type.
+ */
+
+export type Props = {
+ params: LocaleParams;
+ topics: Topic[];
+};
+
+/**
+ * Export `FeaturedSection` component.
+ */
+
+export const FeaturedSection = ({ params, topics }: Props) => {
+ return (
+
+
+
+
+
+ );
+}
+
+/**
+ * `FeaturedSectionContent` component.
+ */
+
+const FeaturedSectionContent = (props: Pick) => {
+ const { topics } = props;
+ const { hits: posts } = useHits();
+ const [scrollProgress, setScrollProgress] = useState(0);
+
+ const scrollTo = useCallback((index: number) => {
+ if(index > 5) {
+ index = 0;
+ }
+
+ if(index < 0) {
+ index = 5;
+ }
+
+ const container = document.getElementById('featured-posts');
+
+ if(!container) {
+ return;
+ }
+
+ const scrollCap = container.scrollWidth - featuredPostsPadding;
+ const scrollPosition = Math.floor(index / 6 * scrollCap);
+
+ container.scrollTo({
+ left: scrollPosition,
+ behavior: 'smooth'
+ });
+ }, []);
+
+ useEffect(() => {
+ const container = document.getElementById('featured-posts');
+ const handleScroll = () => {
+ const container = document.getElementById('featured-posts');
+
+ if(!container) {
+ return;
+ }
+
+ const scrollCap = container.scrollWidth - featuredPostsPadding
+ const scrollPercentage = (Math.min(container?.scrollLeft, scrollCap) / scrollCap) * 100;
+ const scrollPosition = Math.floor((scrollPercentage / 100) * 6);
+
+ setScrollProgress(scrollPosition);
+ };
+
+ container?.addEventListener('scroll', handleScroll);
+
+ return () => {
+ container?.removeEventListener('scroll', handleScroll);
+ };
+ }, []);
+
+ return (
+
+
+ {'Trending Posts'}
+
+
+
+ {posts.map(post => (
+
+ ))}
+
+
+
+
+ }
+ isRound
+ aria-label={'Previous'}
+ onClick={() => scrollTo(scrollProgress - 1)}
+ />
+
+ }
+ isRound
+ aria-label={'Next'}
+ onClick={() => scrollTo(scrollProgress + 1)}
+ />
+
+
+
+
+ {posts.map((_, index) => (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/workspaces/website/src/components/Blog/InfinitePostsSection.tsx b/workspaces/website/src/components/Blog/InfinitePostsSection.tsx
new file mode 100644
index 00000000000..9d3f2f4d9b8
--- /dev/null
+++ b/workspaces/website/src/components/Blog/InfinitePostsSection.tsx
@@ -0,0 +1,68 @@
+/**
+ * Module dependencies
+ */
+
+import { Divider, Grid, HStack } from "@chakra-ui/react";
+import { Button } from "@ui/Button";
+import { useInfiniteHits } from "react-instantsearch-hooks-web";
+import { BlogHit } from "src/pages/posts/CategoryPage";
+import { BlogCard } from "./BlogCard";
+import { Topic } from "@starknet-io/cms-data/src/topics";
+import { EmptySection } from "./EmptySection";
+
+
+/**
+ * `Props` type.
+ */
+
+export type Props = {
+ postType: 'article' | 'video' | 'audio';
+ topics: Topic[];
+};
+
+/**
+ * Export `InfinitePostsSection` component.
+ */
+
+export const InfinitePostsSection = (props: Props) => {
+ const { postType, topics } = props;
+ const { hits, isLastPage, results, showMore } = useInfiniteHits();
+
+ return (
+ <>
+ {(results?.nbHits ?? 0) > 0 ? (
+
+ {hits.map(hit => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+ {!isLastPage && (
+
+
+
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/workspaces/website/src/components/Blog/TopicsList.tsx b/workspaces/website/src/components/Blog/TopicsList.tsx
new file mode 100644
index 00000000000..a6e5095b028
--- /dev/null
+++ b/workspaces/website/src/components/Blog/TopicsList.tsx
@@ -0,0 +1,197 @@
+/**
+ * Module dependencies
+ */
+
+import { As, Flex, Grid, Icon, ResponsiveValue } from "@chakra-ui/react";
+import { Category } from "@starknet-io/cms-data/src/categories";
+import { Topic } from "@starknet-io/cms-data/src/topics";
+import { Button } from "@ui/Button";
+import qs from "qs";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { HiChevronLeft, HiChevronRight } from "react-icons/hi2";
+import { navigate } from "vite-plugin-ssr/client/router";
+
+/**
+ * `commonButtonProps` constant.
+ */
+
+const commonButtonProps = {
+ height: '120%',
+ width: '80px',
+ position: 'absolute' as ResponsiveValue,
+ alignItems: 'center',
+ top: '-1',
+ zIndex: 1,
+ cursor: 'pointer',
+ as: 'button' as As
+}
+
+/**
+ * `Props` type.
+ */
+
+export type Props = {
+ category: Category;
+ topics: readonly Topic[];
+ query: Record;
+ params: LocaleParams & {
+ readonly category?: string;
+ };
+};
+
+/**
+ * Export `TopicList` component.
+ */
+
+export const TopicList = (props: Props) => {
+ const { category, query, params, topics } = props;
+ const activeTopics = (query.topicFilters ?? []) as string[];
+ const [scrollProgress, setScrollProgress] = useState(0);
+ const { isMinScroll, isMaxScroll } = useMemo(() => {
+ if(typeof document === 'undefined') {
+ return {
+ isMinScroll: false,
+ isMaxScroll: false
+ };
+ }
+
+ const container = document?.getElementById('topics-filters');
+ const hasScroll = (container?.scrollWidth ?? 0) > (container?.clientWidth ?? 0);
+
+ return {
+ isMinScroll: hasScroll ? scrollProgress === 0 : false,
+ isMaxScroll: hasScroll ?(scrollProgress + (container?.clientWidth ?? 0)) === container?.scrollWidth : false
+ };
+ }, [scrollProgress]);
+
+ const scrollTo = useCallback((position: number) => {
+ const container = document?.getElementById('topics-filters');
+
+ if(!container) {
+ return;
+ }
+
+ const scrollCap = container.scrollWidth;
+
+ if(position > scrollCap) {
+ position = scrollCap;
+ }
+
+ if(position < 0) {
+ position = 0;
+ }
+
+ container.scrollTo({
+ left: position,
+ behavior: 'smooth'
+ });
+ }, []);
+
+ useEffect(() => {
+ const container = document?.getElementById('topics-filters');
+ const handleScroll = () => {
+ const container = document?.getElementById('topics-filters');
+
+ if(!container) {
+ return;
+ }
+
+ setScrollProgress(container?.scrollLeft);
+ };
+
+ container?.addEventListener('scroll', handleScroll);
+
+ return () => {
+ container?.removeEventListener('scroll', handleScroll);
+ };
+ }, []);
+
+ return (
+
+
+ {!isMinScroll && (
+ scrollTo(scrollProgress - 200)}
+ sx={{
+ _dark: {
+ background: 'linear-gradient(90deg, rgba(11,11,11,1) 50%, rgba(11,11,11,0) 100%)'
+ }
+ }}
+ {...commonButtonProps}
+ >
+
+
+ )}
+
+
+ {topics.map((topic, index) => (
+
+ ))}
+
+
+ {!isMaxScroll && (
+ scrollTo(scrollProgress + 200)}
+ sx={{
+ _dark: {
+ background: 'linear-gradient(90deg, rgba(11,11,11,0), rgba(11,11,11,1) 50%)'
+ }
+ }}
+ {...commonButtonProps}
+ >
+
+
+ )}
+
+ );
+}
diff --git a/workspaces/website/src/components/Button/Button.tsx b/workspaces/website/src/components/Button/Button.tsx
index 1114caa4638..a1d94e66961 100644
--- a/workspaces/website/src/components/Button/Button.tsx
+++ b/workspaces/website/src/components/Button/Button.tsx
@@ -14,7 +14,10 @@ type props = {
| "switch"
| "filter"
| "filterActive"
+ | "smallFilter"
+ | "smallFilterActive"
| "category"
+ | "categoryVertical"
| "icon";
children: React.ReactNode;
toId?: string;
diff --git a/workspaces/website/src/components/Button/ButtonStyles.ts b/workspaces/website/src/components/Button/ButtonStyles.ts
index ab37d43c67e..9b0c78f056a 100644
--- a/workspaces/website/src/components/Button/ButtonStyles.ts
+++ b/workspaces/website/src/components/Button/ButtonStyles.ts
@@ -577,6 +577,63 @@ const category = defineStyle({
},
});
+const categoryVertical = defineStyle({
+ cursor: "pointer",
+ borderRadius: 0,
+ fontWeight: "medium",
+ fontSize: "14px",
+ lineHeight: "14px",
+ padding: "20px 12px",
+ color: "tabs-fg",
+ borderLeftWidth: "1px",
+ borderColor: "tabs-border-bg",
+
+ bg: "tabs-bg",
+ _hover: {
+ bg: "tabs-bg",
+ color: "tabs-hover-fg",
+ },
+ _active: {
+ bg: "tabs-bg",
+ color: "tabs-fg-active",
+ borderColor: "tabs-border-active-bg",
+ },
+});
+
+
+const smallFilter = defineStyle({
+ borderRadius: '8px',
+ fontWeight: "500",
+ fontSize: "12px",
+ lineHeight: "12px",
+ padding: "10px 12px",
+ color: "btn-filter-fg",
+ bg: "btn-filter-bg",
+ _hover: {
+ bg: "btn-filter-hover-bg",
+ color: "btn-filter-hover-fg",
+ },
+ _active: {
+ bg: "btn-filter-active-bg",
+ color: "btn-filter-active-fg",
+ },
+});
+
+const smallFilterActive = defineStyle({
+ borderRadius: '8px',
+ fontWeight: "medium",
+ fontSize: "12px",
+ lineHeight: "12px",
+ padding: "10px 12px",
+ bg: "btn-primary-bg",
+ color: "btn-filter-active-fg",
+ opacity: 0.72,
+ _hover: {
+ bg: "btn-filter-active-hover-bg",
+ color: "btn-filter-active-hover-fg",
+ },
+});
+
const icon = defineStyle({
height: "auto",
padding: "11px",
@@ -649,7 +706,10 @@ export const buttonTheme = defineStyleConfig({
ghost,
filter,
filterActive,
+ smallFilter,
+ smallFilterActive,
category,
+ categoryVertical,
switch: switchButton,
icon
},
diff --git a/workspaces/website/src/components/IconButton/IconButton.tsx b/workspaces/website/src/components/IconButton/IconButton.tsx
index 4a5e9e62041..9f3690dabed 100644
--- a/workspaces/website/src/components/IconButton/IconButton.tsx
+++ b/workspaces/website/src/components/IconButton/IconButton.tsx
@@ -1,75 +1,22 @@
-import { defineStyle } from "@chakra-ui/react";
import { IconButton as ChakraIconButton, IconButtonProps } from "@chakra-ui/react";
import { scrollIntoView } from "../../utils/scrollIntoView";
import { forwardRef } from 'react';
-
-const iconButtonTheme = defineStyle({
- height: "auto",
- color: "darkMode.card",
- bg: "transparent",
- borderColor: "transparent",
- borderWidth: 1,
- _hover: {
- bg: "bg.200",
- _focus: {
- bg: "bg.200",
- borderColor: "transparent",
- borderWidth: 1,
- },
- _dark: {
- bg: "black",
- },
- },
- _active: {
- bg: "transparent",
- boxShadow: "inset 0px 4px 0px rgba(0, 0, 0, 0.1)",
- outlineWidth: 1,
- borderWidth: "1px",
- borderColor: "transparent",
- color: "darkMode.card",
- _focus: {
- bg: "transparent",
- boxShadow: "inset 0px 4px 0px rgba(0, 0, 0, 0.1)",
- outlineWidth: 1,
- borderWidth: "1px",
- borderColor: "transparent"
- },
- _dark: {
- bg: "black",
- color: "btn-outline-active-fg",
- borderColor: "black",
- outlineWidth: 1,
- _focus: {
- bg: "black",
- color: "grey.greyDusk",
- borderColor: "black",
- outlineWidth: 1
- }
- }
- },
- _focus: {
- boxShadow: "none",
- borderColor: "selected.main",
- _dark: {
- boxShadow: "none",
- borderColor: "selected.100",
- borderWidth: "1px",
- borderStyle: "solid"
- }
- },
- _dark: {
- borderColor: "transparent",
- color: "white"
- }
-});
+import { iconButtonTheme } from "./IconButtonStyles";
export interface Props extends IconButtonProps {
toId?: string;
href?: string;
size?: | "default" | "small";
+ variant?: 'outline' | 'primary';
};
-export const IconButton = forwardRef(({ href, toId, size, ...rest }, ref) => {
+export const IconButton = forwardRef(({
+ href,
+ toId,
+ size,
+ variant = 'primary' as keyof typeof iconButtonTheme.variants,
+ ...rest
+}, ref) => {
const paddingValue = size === "small" ? "0" : "11px";
const minWidthValue = size === "small" ? "auto" : "2.5rem";
const handleOnClick = () => {
@@ -83,10 +30,10 @@ export const IconButton = forwardRef(({ href, toId, si
);
diff --git a/workspaces/website/src/components/IconButton/IconButtonStyles.ts b/workspaces/website/src/components/IconButton/IconButtonStyles.ts
new file mode 100644
index 00000000000..fee2e8c0ea3
--- /dev/null
+++ b/workspaces/website/src/components/IconButton/IconButtonStyles.ts
@@ -0,0 +1,140 @@
+/**
+ * Module dependencies
+ */
+
+import { defineStyle, defineStyleConfig } from "@chakra-ui/react";
+
+/**
+ * `primary` variant
+ */
+
+const primary = defineStyle({
+ height: "auto",
+ color: "darkMode.card",
+ bg: "transparent",
+ borderColor: "transparent",
+ borderWidth: 1,
+ _hover: {
+ bg: "bg.200",
+ _focus: {
+ bg: "bg.200",
+ borderColor: "transparent",
+ borderWidth: 1,
+ },
+ _dark: {
+ bg: "black",
+ },
+ },
+ _active: {
+ bg: "transparent",
+ boxShadow: "inset 0px 4px 0px rgba(0, 0, 0, 0.1)",
+ outlineWidth: 1,
+ borderWidth: "1px",
+ borderColor: "transparent",
+ color: "darkMode.card",
+ _focus: {
+ bg: "transparent",
+ boxShadow: "inset 0px 4px 0px rgba(0, 0, 0, 0.1)",
+ outlineWidth: 1,
+ borderWidth: "1px",
+ borderColor: "transparent"
+ },
+ _dark: {
+ bg: "black",
+ color: "btn-outline-active-fg",
+ borderColor: "black",
+ outlineWidth: 1,
+ _focus: {
+ bg: "black",
+ color: "grey.greyDusk",
+ borderColor: "black",
+ outlineWidth: 1
+ }
+ }
+ },
+ _focus: {
+ boxShadow: "none",
+ borderColor: "selected.main",
+ _dark: {
+ boxShadow: "none",
+ borderColor: "selected.100",
+ borderWidth: "1px",
+ borderStyle: "solid"
+ }
+ },
+ _dark: {
+ borderColor: "transparent",
+ color: "white"
+ }
+});
+
+/**
+ * `outline` variant
+ */
+
+const outline = defineStyle({
+ height: "auto",
+ color: "btn-primary-border",
+ bg: "transparent",
+ borderWidth: 1,
+ borderColor: "btn-primary-border",
+ _hover: {
+ bg: "bg.200",
+ opacity: 0.8,
+ _focus: {
+ bg: "bg.200",
+ borderWidth: 1,
+ },
+ _dark: {
+ bg: "black",
+ },
+ },
+ _active: {
+ bg: "transparent",
+ boxShadow: "inset 0px 4px 0px rgba(0, 0, 0, 0.1)",
+ outlineWidth: 1,
+ borderWidth: "1px",
+ borderColor: "btn-primary-border",
+ color: "btn-primary-border",
+ _focus: {
+ bg: "transparent",
+ boxShadow: "inset 0px 4px 0px rgba(0, 0, 0, 0.1)",
+ outlineWidth: 1,
+ borderWidth: "1px",
+ },
+ _dark: {
+ bg: "black",
+ color: "btn-outline-active-fg",
+ borderColor: "black",
+ outlineWidth: 1,
+ _focus: {
+ bg: "black",
+ color: "grey.greyDusk",
+ borderColor: "black",
+ outlineWidth: 1
+ }
+ }
+ },
+ _focus: {
+ boxShadow: "none",
+ _dark: {
+ boxShadow: "none",
+ borderWidth: "1px",
+ borderStyle: "solid"
+ }
+ },
+ _dark: {
+ color: "white"
+ }
+});
+
+/**
+ * Export `iconButtonTheme`
+ */
+
+export const iconButtonTheme = defineStyleConfig({
+ variants: {
+ outline,
+ primary
+ },
+});
diff --git a/workspaces/website/src/pages/posts/@category/@slug/index.page.server.tsx b/workspaces/website/src/pages/post/@slug/index.page.server.tsx
similarity index 74%
rename from workspaces/website/src/pages/posts/@category/@slug/index.page.server.tsx
rename to workspaces/website/src/pages/post/@slug/index.page.server.tsx
index 62736901650..4fab23046f1 100644
--- a/workspaces/website/src/pages/posts/@category/@slug/index.page.server.tsx
+++ b/workspaces/website/src/pages/post/@slug/index.page.server.tsx
@@ -1,10 +1,18 @@
+/**
+ * Module dependencies
+ */
+
import { getCategories } from "@starknet-io/cms-data/src/categories";
import { getPostBySlug } from "@starknet-io/cms-data/src/posts";
import { getTopics } from "@starknet-io/cms-data/src/topics";
import { DocumentProps, PageContextServer } from "src/renderer/types";
-import { Props } from "src/pages/posts/PostPage";
+import { Props } from "src/pages/post/PostPage";
import { getDefaultPageContext } from "src/renderer/helpers";
+/**
+ * Export `onBeforeRender` function.
+ */
+
export async function onBeforeRender(pageContext: PageContextServer) {
const defaultPageContext = await getDefaultPageContext(pageContext);
const { locale } = defaultPageContext;
@@ -19,6 +27,12 @@ export async function onBeforeRender(pageContext: PageContextServer) {
post ,
categories: await getCategories(locale, pageContext.context),
topics: await getTopics(locale, pageContext.context),
+ env: {
+ ALGOLIA_INDEX: import.meta.env.VITE_ALGOLIA_INDEX!,
+ ALGOLIA_APP_ID: import.meta.env.VITE_ALGOLIA_APP_ID!,
+ ALGOLIA_SEARCH_API_KEY: import.meta.env.VITE_ALGOLIA_SEARCH_API_KEY!,
+ SITE_URL: import.meta.env.VITE_SITE_URL,
+ },
params: {
locale,
slug: pageContext.routeParams!.slug!,
diff --git a/workspaces/website/src/pages/post/@slug/index.page.tsx b/workspaces/website/src/pages/post/@slug/index.page.tsx
new file mode 100644
index 00000000000..89b080a8c88
--- /dev/null
+++ b/workspaces/website/src/pages/post/@slug/index.page.tsx
@@ -0,0 +1 @@
+export { Page } from "src/pages/post/PostPage";
diff --git a/workspaces/website/src/pages/post/PostPage.tsx b/workspaces/website/src/pages/post/PostPage.tsx
new file mode 100644
index 00000000000..db1309a1135
--- /dev/null
+++ b/workspaces/website/src/pages/post/PostPage.tsx
@@ -0,0 +1,351 @@
+/**
+ * Module dependencies.
+ */
+
+import {
+ AiFillFacebook,
+ AiFillLinkedin,
+ AiOutlineTwitter
+} from "react-icons/ai";
+
+import {
+ Box,
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ Button,
+ Container,
+ Divider,
+ Flex,
+ Grid,
+ HStack,
+ Heading,
+ Icon,
+ Img,
+ Link,
+} from "@chakra-ui/react";
+
+import { Category } from "@starknet-io/cms-data/src/categories";
+import { Configure, InstantSearch, useHits } from "react-instantsearch-hooks-web";
+import {
+ FacebookShareButton,
+ LinkedinShareButton,
+ TwitterShareButton,
+} from "react-share";
+
+import { Post } from "@starknet-io/cms-data/src/posts";
+import { TableOfContents } from "../(components)/TableOfContents/TableOfContents";
+import { Text } from "@ui/Typography/Text";
+import { Topic } from "@starknet-io/cms-data/src/topics";
+import { Block } from "src/blocks/Block";
+import { YoutubePlayer } from "@ui/YoutubePlayer/YoutubePlayer";
+import { useMemo } from "react";
+import { blocksToTOC } from "../(components)/TableOfContents/blocksToTOC";
+import moment from "moment";
+import qs from "qs";
+import algoliasearch from "algoliasearch";
+import { BlogCard } from "@ui/Blog/BlogCard";
+import { BlogHit } from "../posts/PostsPage";
+
+/**
+ * Export `Props` type.
+ */
+
+export interface Props {
+ readonly params: LocaleParams & {
+ readonly slug: string;
+ };
+ readonly categories: readonly Category[]
+ readonly topics: readonly Topic[]
+ readonly post: Post
+ readonly env: {
+ readonly ALGOLIA_INDEX: string;
+ readonly ALGOLIA_APP_ID: string;
+ readonly ALGOLIA_SEARCH_API_KEY: string;
+ readonly SITE_URL: string;
+ };
+}
+
+/**
+ * Export `MarkdownBlock` type.
+ */
+
+export interface MarkdownBlock {
+ readonly type: "markdown";
+ readonly body: string;
+}
+
+/**
+ * Export `Page` component.
+ */
+
+export function Page(props: Props): JSX.Element {
+ const { params: { slug, locale }, categories, topics, post, env } = props;
+ const category = categories.find((c) => c.id === post.category)!;
+ const videoId = post.post_type === "video" ? post.video?.id : undefined;
+ const shareUrl = `${env.SITE_URL}/post/${slug}`
+ const searchClient = useMemo(() => {
+ return algoliasearch(env.ALGOLIA_APP_ID, env.ALGOLIA_SEARCH_API_KEY);
+ }, [env.ALGOLIA_APP_ID, env.ALGOLIA_SEARCH_API_KEY]);
+
+ return (
+
+
+
+
+
+
+ Blog
+
+
+
+
+
+ {category?.name}
+
+
+
+
+
+ {post.title}
+
+
+
+
+
+
+ {!!post.toc ? (
+
+ ) : null}
+
+
+
+
+ {post.post_type !== "video" ? (
+
+
+
+
+
+
+ {moment(post.published_date).format("MMM DD,YYYY")} ·
+
+
+
+ {post.timeToConsume}
+
+
+
+
+ {`Page last updated ${moment(
+ post?.gitlog?.date
+ ).fromNow()}`}
+
+
+
+ ) : null}
+
+
+ {post.title}
+
+
+
+ {post.short_desc}
+
+
+
+
+ {post.post_type === "video" && (
+
+
+
+
+
+
+ {moment(post.published_date).format("MMM DD,YYYY")} ·
+
+
+
+ {post.timeToConsume}
+
+
+
+
+ {`Page last updated ${moment(
+ post?.gitlog?.date
+ ).fromNow()}`}
+
+
+
+ )}
+
+ {(post.blocks?.length ?? 0) > 0 && (
+
+ {post.blocks?.map((block, i) => (
+
+ ))}
+
+ )}
+
+
+ {post.topic?.map((topic, i) => (
+
+ ))}
+
+
+
+
+ Share this post:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {'May also interest you'}
+
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * `RelatedSection` component.
+ */
+
+function RelatedSection({ topics }: Pick) {
+ const { hits } = useHits();
+
+ return (
+
+ {hits.map(hit => (
+
+ ))}
+
+ )
+}
diff --git a/workspaces/website/src/pages/posts/@category/@slug/index.page.tsx b/workspaces/website/src/pages/posts/@category/@slug/index.page.tsx
deleted file mode 100644
index 84598dbd5fd..00000000000
--- a/workspaces/website/src/pages/posts/@category/@slug/index.page.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { Page } from "src/pages/posts/PostPage";
diff --git a/workspaces/website/src/pages/posts/@category/index.page.server.ts b/workspaces/website/src/pages/posts/@category/index.page.server.ts
index c50ed2155cd..ff1938df133 100644
--- a/workspaces/website/src/pages/posts/@category/index.page.server.ts
+++ b/workspaces/website/src/pages/posts/@category/index.page.server.ts
@@ -1,21 +1,31 @@
import { getCategories } from "@starknet-io/cms-data/src/categories";
import { getTopics } from "@starknet-io/cms-data/src/topics";
import { PageContextServer } from "src/renderer/types";
-import { Props } from "src/pages/posts/PostsPage";
+import { Props } from "src/pages/posts/CategoryPage";
import { getDefaultPageContext } from "src/renderer/helpers";
+import { normalizeCategories } from "src/utils/blog";
+import qs from "qs";
export async function onBeforeRender(pageContext: PageContextServer) {
const defaultPageContext = await getDefaultPageContext(pageContext);
+ const query = qs.parse((pageContext as any)._urlPristine?.split("?")[1] ?? '');
const { locale } = defaultPageContext;
+ const categories = await getCategories(locale, pageContext.context)
const pageProps: Props = {
- categories: await getCategories(locale, pageContext.context),
+ categories: normalizeCategories(categories),
topics: await getTopics(locale, pageContext.context),
env: {
ALGOLIA_INDEX: import.meta.env.VITE_ALGOLIA_INDEX!,
ALGOLIA_APP_ID: import.meta.env.VITE_ALGOLIA_APP_ID!,
ALGOLIA_SEARCH_API_KEY: import.meta.env.VITE_ALGOLIA_SEARCH_API_KEY!,
},
+ query: {
+ postType: query.postType as string,
+ ...!!query.topicFilters && {
+ topicFilters: (Array.isArray(query.topicFilters) ? query.topicFilters : [query.topicFilters]) as string[],
+ }
+ },
params: {
locale,
category: pageContext.routeParams!.category!,
diff --git a/workspaces/website/src/pages/posts/@category/index.page.tsx b/workspaces/website/src/pages/posts/@category/index.page.tsx
index 9db9e8483ee..691fddf9782 100644
--- a/workspaces/website/src/pages/posts/@category/index.page.tsx
+++ b/workspaces/website/src/pages/posts/@category/index.page.tsx
@@ -1 +1 @@
-export { PostsPage as Page } from "src/pages/posts/PostsPage";
+export { CategoryPage as Page } from "src/pages/posts/CategoryPage";
diff --git a/workspaces/website/src/pages/posts/CategoryPage.tsx b/workspaces/website/src/pages/posts/CategoryPage.tsx
new file mode 100644
index 00000000000..381d4ed0d50
--- /dev/null
+++ b/workspaces/website/src/pages/posts/CategoryPage.tsx
@@ -0,0 +1,276 @@
+import {
+ Box,
+ Container,
+ Grid,
+ Flex,
+ ButtonGroup,
+} from "@chakra-ui/react";
+
+import { Button } from "@ui/Button";
+import { useMemo, } from "react";
+import algoliasearch from "algoliasearch/lite";
+import {
+ InstantSearch,
+ Configure,
+} from "react-instantsearch-hooks-web";
+
+import type { Topic } from "@starknet-io/cms-data/src/topics";
+import { Heading } from "@ui/Typography/Heading";
+import { CategoryList } from "@ui/Blog/CategoryList";
+import { BlogSection } from "@ui/Blog/BlogSection";
+import { NormalizedCategory } from "src/utils/blog";
+import qs from "qs";
+import { HiFilm, HiOutlineBookOpen, HiPlay } from "react-icons/hi2";
+import { TopicList } from "@ui/Blog/TopicsList";
+import { InfinitePostsSection } from "@ui/Blog/InfinitePostsSection";
+
+export interface Props extends LocaleProps {
+ readonly categories: readonly NormalizedCategory[];
+ readonly topics: readonly Topic[];
+ readonly params: LocaleParams & {
+ readonly category?: string;
+ };
+ readonly query: {
+ readonly topicFilters?: readonly string[];
+ readonly postType?: string;
+ }
+ readonly env: {
+ readonly ALGOLIA_INDEX: string;
+ readonly ALGOLIA_APP_ID: string;
+ readonly ALGOLIA_SEARCH_API_KEY: string;
+ };
+}
+
+/**
+ * `postTypes` constant.
+ */
+
+const postTypes = [
+ {
+ value: 'article',
+ label: 'Articles',
+ icon:
+ },
+ {
+ value: 'video',
+ label: 'Videos',
+ icon:
+ },
+ {
+ value: 'audio',
+ label: 'Audios',
+ icon:
+ }
+]
+
+/**
+ * `CategoryPage` component.
+ */
+
+export function CategoryPage({
+ env,
+ params,
+ query,
+ categories,
+ topics,
+}: Props): JSX.Element | null {
+ const searchClient = useMemo(() => {
+ return algoliasearch(env.ALGOLIA_APP_ID, env.ALGOLIA_SEARCH_API_KEY);
+ }, [env.ALGOLIA_APP_ID, env.ALGOLIA_SEARCH_API_KEY]);
+
+ const category = categories.find(category =>
+ category.slug === params.category
+ ) as NormalizedCategory;
+
+ const hasChildren = (category?.children?.length ?? 0) > 0;
+
+
+ return (
+
+
+
+
+
+
+
+ {category.name}
+
+
+
+ {postTypes.map((postType) => (
+
+ ))}
+
+
+
+
+
+
+ {hasChildren && !query.postType && (
+
+ {category.children.map(category => (
+
+
+
+
+
+ ))}
+
+ )}
+
+ {!hasChildren && !query.postType && (
+
+ {postTypes.map(postType => (
+
+
+
+
+
+ ))}
+
+ )}
+
+ {query.postType && (
+
+
+
+
+
+ )}
+
+
+
+ );
+}
+
+type VideoData = {
+ etag: string;
+ id: string;
+ kind: string;
+ snippet: object;
+ contentDetails: {
+ duration: string;
+ }
+}
+
+type Video = {
+ data: VideoData;
+ url: string;
+ id: string;
+}
+
+export type BlogHit = {
+ readonly id: string;
+ readonly slug: string;
+ readonly title: string;
+ readonly image: string;
+ readonly category: string;
+ readonly topic: string[];
+ readonly short_desc: string;
+ readonly locale: string;
+ readonly filepath: string;
+ readonly post_type: string;
+ readonly published_date: string;
+ readonly featured_post: boolean;
+ readonly blocks: Array;
+ readonly video: Video;
+ readonly timeToConsume: string;
+};
+
+interface Block {
+ body?: string;
+ type?: string;
+}
diff --git a/workspaces/website/src/pages/posts/PostPage.tsx b/workspaces/website/src/pages/posts/PostPage.tsx
deleted file mode 100644
index 45fe72d2a68..00000000000
--- a/workspaces/website/src/pages/posts/PostPage.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Post } from "@starknet-io/cms-data/src/posts";
-import { Category } from "@starknet-io/cms-data/src/categories";
-import { Topic } from "@starknet-io/cms-data/src/topics";
-import PostByCategory from "./PostByCategory";
-
-export interface Props {
- readonly params: LocaleParams & {
- readonly slug: string;
- };
- readonly categories: readonly Category[]
- readonly topics: readonly Topic[]
- readonly post: Post
-}
-
-export interface MarkdownBlock {
- readonly type: "markdown";
- readonly body: string;
-}
-
-export function Page({ params: { slug, locale }, categories, topics, post }: Props): JSX.Element {
- const category = categories.find((c) => c.id === post.category)!;
- return (
-
- )
-}
diff --git a/workspaces/website/src/pages/posts/PostsPage.tsx b/workspaces/website/src/pages/posts/PostsPage.tsx
index 56d30a1ea12..45dda6d884c 100644
--- a/workspaces/website/src/pages/posts/PostsPage.tsx
+++ b/workspaces/website/src/pages/posts/PostsPage.tsx
@@ -1,34 +1,21 @@
import {
- BreadcrumbItem,
- BreadcrumbLink,
- Breadcrumb,
Box,
Container,
Flex,
- HStack,
- Divider,
- Grid,
} from "@chakra-ui/react";
-import { Button } from "@ui/Button";
-import moment from "moment";
-import * as ArticleCard from "@ui/ArticleCard/ArticleCard";
-import { useMemo, useState, useEffect } from "react";
+
+import { useMemo } from "react";
import algoliasearch from "algoliasearch/lite";
import {
InstantSearch,
Configure,
- useRefinementList,
} from "react-instantsearch-hooks-web";
+
import type { Category } from "@starknet-io/cms-data/src/categories";
-import { PageLayout } from "@ui/Layout/PageLayout";
import type { Topic } from "@starknet-io/cms-data/src/topics";
-import { useInfiniteHits } from "react-instantsearch-hooks-web";
-import { Heading } from "@ui/Typography/Heading";
-import { RefinementListProps } from "react-instantsearch-hooks-web/dist/es/ui/RefinementList";
-import MobileFiltersButton from "../(components)/MobileFilter/MobileFiltersButton";
-import useMobileFiltersDrawer from "../(components)/MobileFilter/useMobileFiltersDrawer";
-import MobileFiltersDrawer from "../(components)/MobileFilter/MobileFiltersDrawer";
-import { navigate } from "vite-plugin-ssr/client/router";
+import { CategoryList } from "@ui/Blog/CategoryList";
+import { BlogSection } from "@ui/Blog/BlogSection";
+import { FeaturedSection } from "@ui/Blog/FeaturedSection";
export interface Props extends LocaleProps {
readonly categories: readonly Category[];
@@ -52,15 +39,13 @@ export function PostsPage({
const searchClient = useMemo(() => {
return algoliasearch(env.ALGOLIA_APP_ID, env.ALGOLIA_SEARCH_API_KEY);
}, [env.ALGOLIA_APP_ID, env.ALGOLIA_SEARCH_API_KEY]);
-
- // const searchParams = useSearchParams();
const category = categories.find((c) => c.slug === params.category);
return (
-
-
-
-
-
-
- );
-}
-const PostsPageLayout = ({
- params,
- categories,
- topics,
-}: Pick) => {
- const category = categories.find((c) => c.slug === params.category);
-
- const { items: topicsItems, refine: refineTopics } = useRefinementList({
- attribute: "topic",
- limit: 50,
- sortBy: ["count:desc"],
- });
-
- const {
- isOpen,
- onOpen,
- onClose,
- setFilterOpen,
- handleFilterClick,
- filtersCounts,
- selectedFilters,
- setSelectedFilters
- } = useMobileFiltersDrawer(topicsItems);
-
- function mapSelectedFilters() {
- let result: { topic?: string[] } = {};
- let topicsFilteredValues = topicsItems
- .filter(item => item.isRefined)
- .map(item => item.value);
- if (topicsFilteredValues.length > 0) {
- result["topic"] = topicsFilteredValues;
- }
- return result;
- }
-
- const handleModalClose = () => {
- onClose();
- setSelectedFilters(mapSelectedFilters());
- }
-
- const handleApplyChanges = () => {
- if (selectedFilters["topic"]?.length) {
- selectedFilters["topic"].map((topic) => {
- refineTopics(topic);
- });
- } else {
- topicsItems.map((topic) => {
- topic.isRefined && refineTopics(topic.value);
- });
- }
- }
-
- const handleApplyFilters = () => {
- handleApplyChanges();
- setFilterOpen(false);
- };
-
- const handleClearFilters = () => {
- handleApplyChanges();
- setSelectedFilters({});
- };
-
- return (
-
- }
- breadcrumbs={
-
-
-
- Home
-
-
-
-
- Community
-
-
+
+
-
- Blog
-
-
- }
- leftAside={
-
-
- Topics
-
-
-
- }
- main={
-
-
-
-
+
-
-
-
-
- }
- />
- );
-};
-
-type CustomTopicsProps = {
- topics: readonly Topic[];
- items: RefinementListProps["items"];
- refineTopics: any;
- selectedFilters?: any;
- isDesktop?: boolean;
-};
-function CustomTopics({
- topics,
- items,
- refineTopics,
- selectedFilters,
- isDesktop = true
-}: CustomTopicsProps) {
- // const router = useRouter();
- // const pathname = usePathname()!;
- // const searchParams = useSearchParams();
- // const topicSet = useMemo(() => {
- // return new Set(searchParams.get("topic")?.split(",") ?? []);
- // }, [searchParams]);
- const checkIfFilterExists = (role: string, filter: string, selectedFilters: { [key: string]: string[] }) => {
- const rolesA = selectedFilters[filter];
- return rolesA && rolesA.includes(role);
- };
- const topicsDict = useMemo(() => {
- return topics.reduce((acc, topic) => {
- acc[topic.id] = topic;
- return acc;
- }, {} as Record);
- }, [topics]);
- const validTopics = useMemo(() => {
- return items.filter((topic) => topicsDict[topic.value] != null);
- }, [topicsDict, items]);
-
- return (
-
- {validTopics.map((topic, i) => (
-
- ))}
+
+
+ ))}
+
+
+
);
}
-function CustomCategories({
- categories,
- params,
-}: Pick) {
- return (
-
-
-
-
- {categories.map((category) => (
-
-
-
- ))}
-
- );
-}
-
type VideoData = {
etag: string;
id: string;
@@ -353,13 +121,13 @@ type Video = {
id: string;
}
-type Hit = {
+export type BlogHit = {
readonly id: string;
readonly slug: string;
readonly title: string;
readonly image: string;
readonly category: string;
- readonly topic: string;
+ readonly topic: string[];
readonly short_desc: string;
readonly locale: string;
readonly filepath: string;
@@ -375,124 +143,3 @@ interface Block {
body?: string;
type?: string;
}
-
-function CustomHits({ categories, params }: Pick) {
- const category = categories.find((c) => c.slug === params.category);
- const { hits, showMore, isLastPage } = useInfiniteHits();
- const [featuredHit, setFeaturedHit] = useState();
- const [filteredHits, setFilteredHits] = useState([]);
- const [featuredHitDate, setFeaturedHitDate] = useState();
- const [featuredHitCategory, setFeaturedHitCategory] = useState(categories[0]);
-
- useEffect(() => {
- const handleResize = () => {
- if (hits) {
- if (category) {
- if (window.innerWidth > 992 && category.show_custom_featured_post && category.custom_featured_post) {
- setFilteredHits(hits.filter(hit => hit.id !== category.custom_featured_post));
- setFeaturedHit(hits.find(hit => hit.id === category.custom_featured_post))
- } else if (window.innerWidth > 992 && !(category.show_custom_featured_post && category.custom_featured_post)) {
- setFeaturedHit(hits[0])
- setFilteredHits(hits.slice(1));
- } else {
- setFilteredHits(hits);
- }
- } else {
- if (window.innerWidth > 992 && hits.some(obj => obj.featured_post === true)) {
- setFilteredHits(hits.filter(hit => hit.featured_post !== true));
- setFeaturedHit(hits.find(hit => hit.featured_post === true))
- } else if (window.innerWidth > 992 && !hits.some(obj => obj.featured_post === true)) {
- setFeaturedHit(hits[0])
- setFilteredHits(hits.slice(1));
- } else {
- setFilteredHits(hits);
- }
- }
- }
- }
- handleResize();
- window.addEventListener('resize', handleResize)
- }, [hits])
- useEffect(() => {
- if (hits && featuredHit) {
- setFeaturedHitDate(moment(featuredHit.published_date).format("MMM DD, YYYY"));
- setFeaturedHitCategory(categories.find((c) => c.id === featuredHit.category) || categories[0])
- }
-
- }, [hits, categories, featuredHit])
- return (
- <>
- {featuredHit && window.innerWidth > 992 &&
-
-
-
-
-
-
-
-
-
- }
-
- {filteredHits.map((hit, i) => {
- // todo: add a featured image once we have image templates in place
- const date = moment(hit.published_date).format("MMM DD, YYYY");
- const category = categories.find((c) => c.id === hit.category);
-
- return (
-
-
-
-
- {category && }
-
-
-
-
- );
- })}
-
- {!isLastPage && (
-
-
-
-
-
- )}
- >
- );
-}
diff --git a/workspaces/website/src/utils/blog.ts b/workspaces/website/src/utils/blog.ts
new file mode 100644
index 00000000000..adb81b96f18
--- /dev/null
+++ b/workspaces/website/src/utils/blog.ts
@@ -0,0 +1,42 @@
+/**
+ * Module dependencies
+ */
+
+import { Category } from "@starknet-io/cms-data/src/categories";
+
+/**
+ * Export `NormalizedCategory` type.
+ */
+
+export type NormalizedCategory = Omit & {
+ children: Omit[];
+};
+
+/**
+ * Export `normalizeCategories` util.
+ */
+
+export function normalizeCategories(categories: readonly Category[]) {
+ const normalizedCategories = categories.map((category) => ({
+ ...category,
+ children: [] as Omit[],
+ }));
+
+ const categoriesMap = new Map(
+ normalizedCategories.map((category) => [category.id, category])
+ );
+
+ for (const category of normalizedCategories) {
+ let parentCategory = categoriesMap.get(category.parentCategory ?? '');
+
+ while(parentCategory?.parentCategory) {
+ parentCategory = categoriesMap.get(parentCategory.parentCategory);
+ }
+
+ if (parentCategory) {
+ parentCategory.children.push(category);
+ }
+ }
+
+ return normalizedCategories;
+}