diff --git a/_data/settings/featured-sections.yml b/_data/settings/featured-sections.yml new file mode 100644 index 00000000000..a0ea7f2ab63 --- /dev/null +++ b/_data/settings/featured-sections.yml @@ -0,0 +1,9 @@ +items: + - categroy: community-and-events + - categroy: community-calls + - categroy: ecosystem + - categroy: engineering + - categroy: 757155c6-ce07-49f1-af21-907b7e0b1cb1 + - categroy: governance + - categroy: stark-math + - categroy: stark-struck diff --git a/package.json b/package.json index 9b059eddcc7..cfe7b6518df 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "cors": "^2.8.5", "date-fns": "^2.30.0", "eslint": "^8.39.0", - "framer-motion": "^10.12.4", + "framer-motion": "^10.16.16", "fsevents": "^2.3.2", "highlight.js": "^11.8.0", "highlightjs-cairo": "^0.2.0", 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/collections/posts.ts b/workspaces/cms-config/src/collections/posts.ts index 2f4db752fb8..0ef97fd4181 100644 --- a/workspaces/cms-config/src/collections/posts.ts +++ b/workspaces/cms-config/src/collections/posts.ts @@ -82,6 +82,11 @@ export const postsCollectionConfig = { ], default: "article", }, + { + name: "isFeatured", + label: "Is featured post", + widget: "boolean", + }, { name: "title", label: "Post Title", @@ -113,6 +118,7 @@ export const postsCollectionConfig = { name: "category", label: "Category", widget: "relation", + multiple: true, collection: "categories", search_fields: ["name"], value_field: "id", diff --git a/workspaces/cms-config/src/collections/settings.ts b/workspaces/cms-config/src/collections/settings.ts index 537b3f9b00a..2b776261f4f 100644 --- a/workspaces/cms-config/src/collections/settings.ts +++ b/workspaces/cms-config/src/collections/settings.ts @@ -131,6 +131,31 @@ export const settingsCollectionConfig = { }, ], }, + { + label: "Featured Sections", + name: "featured-sections", + file: `_data/settings/featured-sections.yml`, + crowdin: false, + fields: [ + { + label: "Category sections", + name: "items", + widget: "list", + fields: [ + { + name: 'category', + label: 'Category', + widget: 'relation', + collection: 'categories', + search_fields: ['name'], + value_field: 'id', + display_fields: ['name'], + options_length: 300 + }, + ], + }, + ], + }, { label: "Wallets", name: "wallets", 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/cms-data/src/settings/featured-settings.ts b/workspaces/cms-data/src/settings/featured-settings.ts new file mode 100644 index 00000000000..d20843071f8 --- /dev/null +++ b/workspaces/cms-data/src/settings/featured-settings.ts @@ -0,0 +1,28 @@ +/** + * Module dependencies. + */ + +import { getJSON } from "@starknet-io/cms-utils/src/index"; + +/** + * Export FeaturedSections type. + */ + +export type FeaturedSections = string[] + +/** + * Export `getFeaturedSections` function. + */ +export async function getFeaturedSections( + context: EventContext<{}, any, Record> +): Promise { + try { + const sections = await getJSON("data/featured-sections/featured-sections", context) + + return sections.items.map((item: { category: string }) => item.category); + } catch (cause) { + throw new Error("getFeaturedSection failed!", { + cause, + }); + } +} diff --git a/workspaces/cms-scripts/src/index.ts b/workspaces/cms-scripts/src/index.ts index 6f07c48d3a7..b6b7261235c 100644 --- a/workspaces/cms-scripts/src/index.ts +++ b/workspaces/cms-scripts/src/index.ts @@ -276,8 +276,12 @@ for (const locale of locales) { } const redirects = await yaml("_data/settings/redirects.yml"); - await write(`workspaces/website/redirects.json`, redirects); + +await fs.mkdir("public/data/featured-sections", { recursive: true }); +const featuredSections = await yaml("_data/settings/featured-sections.yml"); +await write(`public/data/featured-sections/featured-sections.json`, featuredSections); + await createRoadmapDetails() await createAnnouncementDetails() await createSharedData() diff --git a/workspaces/website/src/components/ArticleCard/ArticleCard.tsx b/workspaces/website/src/components/ArticleCard/ArticleCard.tsx index f8efcb9691e..c0f962ba8cf 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; @@ -25,7 +28,7 @@ type RootProps = { const Root = ({ children, href, type = "grid", sx }: RootProps) => { return ( - + { ); }; -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/BlogBreadcrumbs.tsx b/workspaces/website/src/components/Blog/BlogBreadcrumbs.tsx new file mode 100644 index 00000000000..61ec65688ac --- /dev/null +++ b/workspaces/website/src/components/Blog/BlogBreadcrumbs.tsx @@ -0,0 +1,66 @@ +/** + * Module dependencies + */ + +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + Flex, + FlexProps +} from "@chakra-ui/react"; + +import { IoHomeOutline } from "react-icons/io5"; + +/** + * `Props` type. + */ + +type Props = FlexProps & { + locale: string; + title: string; +} + +/** + * Export `BlogBreadcrumbs` component. + */ + +export const BlogBreadcrumbs = ({ locale, title, ...rest }: Props) => ( + + + + + + + {'Home'} + + + + + {'Content'} + + + + + + {title} + + + + +); diff --git a/workspaces/website/src/components/Blog/BlogCard.tsx b/workspaces/website/src/components/Blog/BlogCard.tsx new file mode 100644 index 00000000000..4bd9d1cfb8a --- /dev/null +++ b/workspaces/website/src/components/Blog/BlogCard.tsx @@ -0,0 +1,135 @@ +/** + * 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, BoxProps, Flex, Grid, Icon, useBreakpointValue } from "@chakra-ui/react"; +import { IoPlaySharp } from "react-icons/io5"; + +/** + * `Props` type. + */ + +type Props = BoxProps & { + isFeatured?: boolean; + post: BlogHit; + topics: Topic[] +}; + + +/** + * Export `BlogCard` component. + */ + +export const BlogCard = ({ isFeatured, post, topics, ...rest }: Props) => { + const isMobile = useBreakpointValue({ base: true, md: false }) + 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} + + ))} + + )} + + )} + + + +