diff --git a/components/AdminProvider.tsx b/components/AdminProvider.tsx new file mode 100644 index 00000000..c0f69a52 --- /dev/null +++ b/components/AdminProvider.tsx @@ -0,0 +1,28 @@ +import { setAdmin } from "redux/slices/admin"; +import { useAppDispatch } from "redux/typesHooks"; + +import useIsomorphicLayoutEffect from "./useIsomorphicLayoutEffect"; + +interface Props { + children: JSX.Element; +} + +const getAdmin = (): boolean | undefined => { + return localStorage.admin === "true"; +}; + +const getInitialAdmin = (): boolean => getAdmin() || false; + +const AdminProvider = ({ children }: Props) => { + const dispatch = useAppDispatch(); + + useIsomorphicLayoutEffect(() => { + const admin = getInitialAdmin(); + + dispatch(setAdmin(admin)); + }, [dispatch]); + + return children; +}; + +export default AdminProvider; diff --git a/components/Intro.tsx b/components/Intro.tsx index 2b87c239..363cdd1f 100644 --- a/components/Intro.tsx +++ b/components/Intro.tsx @@ -1,30 +1,219 @@ +import { Box, Typography } from "@mui/material"; +import { keyframes } from "@mui/system"; import config from "config"; +import { useEffect, useRef, useState } from "react"; +import { useDispatch } from "react-redux"; +import { setAdmin } from "redux/slices/admin"; +import { useAppSelector } from "redux/typesHooks"; -const Intro = () => ( -
-
-
-
-
- author-avatar -
-
{config.author_name}
-
{config.author_position}
-
-
-

Hello, I am {config.author_name}

-

- {config.site_description} -

-
-
-
-
-); +const fadeIn = keyframes({ + "0%": { + opacity: 0, + }, + "100%": { + opacity: 1, + }, +}); + +const fadeOut = keyframes({ + "0%": { + opacity: 1, + }, + "100%": { + opacity: 0, + }, +}); + +const animate = (animationName: string) => ({ + animationName: `${animationName}`, + animationDuration: "500ms", + animationFillMode: "forwards", +}); + +const Intro = () => { + const dispatch = useDispatch(); + const admin = useAppSelector((state) => state.admin); + + const [isFirstRender, setIsFirstRender] = useState(true); + + const imageAvatarRef = useRef(null); + + const handleClick = () => { + if (admin) { + dispatch(setAdmin(false)); + } else { + dispatch(setAdmin(true)); + } + }; + + useEffect(() => { + const imageWrapper = imageAvatarRef.current; + + if (admin) { + imageWrapper?.setAttribute("admin", ""); + setIsFirstRender(false); + } + + if (!admin && !isFirstRender) { + imageWrapper?.setAttribute("closing", ""); + imageWrapper?.addEventListener( + "animationend", + () => { + imageWrapper?.removeAttribute("closing"); + }, + { once: true } + ); + imageWrapper?.removeAttribute("admin"); + } + }, [admin, isFirstRender]); + + return ( + ({ + py: 8.75, + background: theme.palette.mode === "light" ? "#f8fafc" : "#070809", + color: theme.palette.mode === "light" ? "#707070" : "#f2f5f7", + })} + > + + {/* check, do we need to rewrite this className? */} + ({ + display: ["-ms-flexbox", "flex"], + msFlexAlign: "center", + alignItems: "center", + [theme.breakpoints.down(768)]: { + textAlign: "center", + display: "block", + }, + })} + > + ({ + position: "relative", + msFlexNegative: 0, + flexShrink: 0, + marginLeft: "auto", + textAlign: "center", + maxWidth: "136px", + msFlexOrder: 2, + order: 2, + [theme.breakpoints.down(768)]: { + width: "100%", + maxWidth: "100%", + marginBottom: "20px", + }, + })} + > + + author-avatar + + ({ + color: theme.palette.mode === "light" ? "#3a3a3a" : "white", + fontSize: "16px", + fontWeight: 700, + textTransform: "uppercase", + marginBottom: "4px", + })} + > + {config.author_name} + + + {config.author_position} + + + ({ + pr: 17.5, + [theme.breakpoints.down(1260)]: { + pr: 10, + }, + [theme.breakpoints.down(1020)]: { + pr: 8.75, + }, + [theme.breakpoints.down(768)]: { + pr: 0, + }, + })} + > + ({ + color: theme.palette.mode === "light" ? "#3a3a3a" : "white", + lineHeight: 1.28, + fontWeight: "bold", + fontSize: "50px", + marginBottom: "17px", + letterSpacing: 0, + [theme.breakpoints.down(1020)]: { + fontSize: "40px", + }, + [theme.breakpoints.down(481)]: { + fontSize: "30px", + mb: 3.25, + }, + })} + > + Hello, I am {config.author_name} + + + {config.site_description} + + + + + + ); +}; export default Intro; diff --git a/components/Post/LatestPosts.tsx b/components/Post/LatestPosts.tsx index 1ddb0a8f..07e80abf 100644 --- a/components/Post/LatestPosts.tsx +++ b/components/Post/LatestPosts.tsx @@ -1,7 +1,4 @@ -import { - isPostADraft, - isPostInTheFuture, -} from "helpers/checkOfDraftOrFuturePost"; +import isUpcomingPost from "helpers/isUpcomingPost"; import { PostDocumentWithoutContent } from "interfaces"; import Image from "next/image"; import Link from "next/link"; @@ -28,11 +25,7 @@ const LatestPosts = ({ latestPosts }: Props) => { height="102" sizes="100vw" style={{ - filter: - isPostADraft(post) || isPostInTheFuture(post) - ? "grayscale(50%)" - : "none", - + filter: isUpcomingPost(post) ? "grayscale(50%)" : "none", objectFit: "cover", }} /> diff --git a/components/Post/PostContent.tsx b/components/Post/PostContent.tsx index 4409d681..316a3861 100644 --- a/components/Post/PostContent.tsx +++ b/components/Post/PostContent.tsx @@ -4,6 +4,7 @@ import { isPostInTheFuture, } from "helpers/checkOfDraftOrFuturePost"; import getFirstParagraph from "helpers/getFirstParagraph"; +import isUpcomingPost from "helpers/isUpcomingPost"; import toHumanReadableDate from "helpers/toHumanReadableDate"; import { PostDocument } from "interfaces"; import Head from "next/head"; @@ -47,10 +48,7 @@ const PostContent = ({ post }: Props) => { width={790} height={394} style={{ - filter: - isPostADraft(post) || isPostInTheFuture(post) - ? "grayscale(50%)" - : "none", + filter: isUpcomingPost(post) ? "grayscale(50%)" : "none", borderRadius: "3px", }} /> diff --git a/components/PostCard.tsx b/components/PostCard.tsx index d67b3118..6e917238 100644 --- a/components/PostCard.tsx +++ b/components/PostCard.tsx @@ -2,6 +2,7 @@ import { isPostADraft, isPostInTheFuture, } from "helpers/checkOfDraftOrFuturePost"; +import isUpcomingPost from "helpers/isUpcomingPost"; import { PostDocumentWithoutContent } from "interfaces"; import Image from "next/image"; import Link from "next/link"; @@ -55,13 +56,7 @@ const PostCard = ({ post }: Props) => { {isPostADraft(post) && } {isPostInTheFuture(post) && } -
+
{ height="378" sizes="100vw" style={{ - filter: - isPostADraft(post) || isPostInTheFuture(post) - ? "grayscale(50%)" - : "none", + filter: isUpcomingPost(post) ? "grayscale(50%)" : "none", objectFit: "cover", }} diff --git a/components/Tags.tsx b/components/Tags.tsx index a828d2d4..e2c98b59 100644 --- a/components/Tags.tsx +++ b/components/Tags.tsx @@ -5,10 +5,12 @@ import { useAppDispatch, useAppSelector } from "redux/typesHooks"; interface Props { uniqueTags: string[]; + countOfPostsInTags: number[]; } -const Tags = ({ uniqueTags }: Props) => { +const Tags = ({ uniqueTags, countOfPostsInTags }: Props) => { const selectedTags = useAppSelector((state) => state.selectedTags); + const admin = useAppSelector((state) => state.admin); const dispatch = useAppDispatch(); @@ -27,7 +29,7 @@ const Tags = ({ uniqueTags }: Props) => { > ALL - {uniqueTags.map((uniqueTag) => ( + {uniqueTags.map((uniqueTag, index) => (
  • { }} > {uniqueTag} + {admin && `[${countOfPostsInTags[index]}]`}
  • ))} diff --git a/components/UpcomingPosts.tsx b/components/UpcomingPosts.tsx new file mode 100644 index 00000000..20b77eef --- /dev/null +++ b/components/UpcomingPosts.tsx @@ -0,0 +1,37 @@ +import { Box, Divider, Typography } from "@mui/material"; +import { PostDocumentWithoutContent } from "interfaces"; + +import PostCard from "./PostCard"; + +interface Props { + posts: PostDocumentWithoutContent[]; +} + +const UpcomingPosts = ({ posts }: Props) => ( + + + ({ + color: theme.palette.mode === "light" ? "#3a3a3a" : "white", + lineHeight: 1.28, + fontWeight: 700, + fontSize: "43px", + mb: 9, + ["@media (max-width: 480px)"]: { + fontSize: "28px", + }, + })} + > + Upcoming Posts + + +
      + {posts.map((post, index) => ( + + ))} +
    + +
    +); + +export default UpcomingPosts; diff --git a/helpers/isUpcomingPost.ts b/helpers/isUpcomingPost.ts new file mode 100644 index 00000000..01fb40b0 --- /dev/null +++ b/helpers/isUpcomingPost.ts @@ -0,0 +1,7 @@ +import { PostDocumentWithoutContent } from "interfaces"; + +import { isPostADraft, isPostInTheFuture } from "./checkOfDraftOrFuturePost"; + +export default function isUpcomingPost(post: PostDocumentWithoutContent) { + return isPostADraft(post) || isPostInTheFuture(post); +} diff --git a/helpers/markdownDocumentsReader.ts b/helpers/markdownDocumentsReader.ts index 9a9bff60..f3525125 100644 --- a/helpers/markdownDocumentsReader.ts +++ b/helpers/markdownDocumentsReader.ts @@ -3,7 +3,7 @@ import matter from "gray-matter"; import { PostDocument, PostDocumentWithoutContent } from "interfaces"; import { join } from "path"; -import { isPostADraft, isPostInTheFuture } from "./checkOfDraftOrFuturePost"; +import isUpcomingPost from "./isUpcomingPost"; const documentsDirectory = join(process.cwd(), "content/posts"); @@ -31,7 +31,7 @@ export function getAllPostDocuments(): PostDocumentWithoutContent[] { return true; } - if (isPostADraft(post) || isPostInTheFuture(post)) { + if (isUpcomingPost(post)) { return false; } @@ -45,3 +45,17 @@ export function getAllPostDocuments(): PostDocumentWithoutContent[] { return JSONSerialize(docs); } + +export function getUpcomingPosts(): PostDocumentWithoutContent[] { + const slugs = fs.readdirSync(documentsDirectory); + const docs = slugs + .map((slug) => getPostDocumentBySlug(slug)) + .filter((post: PostDocument) => isUpcomingPost(post)) + .sort((post1, post2) => (post1.date > post2.date ? -1 : 1)) + .map((post) => { + const { content, ...postWithoutContent } = post; + return postWithoutContent; + }); + + return JSONSerialize(docs); +} diff --git a/pages/_app.tsx b/pages/_app.tsx index 53aa9262..2ffa7638 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,19 +1,21 @@ import "../styles/root.scss"; +import AdminProvider from "components/AdminProvider"; +import GoogleAnalytics from "components/GoogleAnalytics"; +import ThemeProvider from "components/MUI/ThemeProvider"; import type { AppProps } from "next/app"; import { Provider } from "react-redux"; import store from "redux/store"; -import GoogleAnalytics from "@/components/GoogleAnalytics"; -import ThemeProvider from "@/components/MUI/ThemeProvider"; - const MyApp = ({ Component, pageProps }: AppProps) => ( - - - + + + + + ); diff --git a/pages/api/getUpcomingPosts.ts b/pages/api/getUpcomingPosts.ts new file mode 100644 index 00000000..f65313d6 --- /dev/null +++ b/pages/api/getUpcomingPosts.ts @@ -0,0 +1,8 @@ +import { getUpcomingPosts } from "helpers/markdownDocumentsReader"; +import type { NextApiRequest, NextApiResponse } from "next"; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + const posts = getUpcomingPosts(); + + res.status(200).json(posts); +} diff --git a/pages/index.tsx b/pages/index.tsx index 747a99c0..04db01c0 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,5 +1,8 @@ import config from "config"; -import { getAllPostDocuments } from "helpers/markdownDocumentsReader"; +import { + getAllPostDocuments, + getUpcomingPosts, +} from "helpers/markdownDocumentsReader"; import { PostDocumentWithoutContent } from "interfaces"; import type { NextPage } from "next"; import Head from "next/head"; @@ -12,8 +15,12 @@ import Pagination from "@/components/Pagination"; import Posts from "@/components/Posts"; import StandWithUkraine from "@/components/StandWithUkraine"; import Tags from "@/components/Tags"; +import UpcomingPosts from "@/components/UpcomingPosts"; -const Home: NextPage<{ posts: PostDocumentWithoutContent[] }> = ({ posts }) => { +const Home: NextPage<{ + posts: PostDocumentWithoutContent[]; + upcomingPosts: PostDocumentWithoutContent[]; +}> = ({ posts, upcomingPosts }) => { const tags = posts.map((post) => post.tags).flat(); const tagsFrequency = tags.reduce((acc, tag) => { @@ -28,6 +35,7 @@ const Home: NextPage<{ posts: PostDocumentWithoutContent[] }> = ({ posts }) => { const sortedTags = Object.entries(tagsFrequency).sort((a, b) => b[1] - a[1]); const uniqueSortedTags = sortedTags.map((tag) => tag[0]); + const countOfPostsInTags = sortedTags.map((tag) => tag[1]); const selectedTags = useAppSelector((state) => state.selectedTags); const filteredPosts = posts.filter((post) => { @@ -45,6 +53,8 @@ const Home: NextPage<{ posts: PostDocumentWithoutContent[] }> = ({ posts }) => { (currentPage + 1) * config.posts_per_page ); + const admin = useAppSelector((state) => state.admin); + return ( <> @@ -62,8 +72,11 @@ const Home: NextPage<{ posts: PostDocumentWithoutContent[] }> = ({ posts }) => {
    - {/* TODO: Implement tags count for the admin user */} - + {admin && } +
    @@ -75,10 +88,12 @@ const Home: NextPage<{ posts: PostDocumentWithoutContent[] }> = ({ posts }) => { export const getStaticProps = async () => { const posts = getAllPostDocuments(); + const upcomingPosts = getUpcomingPosts(); return { props: { posts, + upcomingPosts, }, }; }; diff --git a/redux/slices/admin.ts b/redux/slices/admin.ts new file mode 100644 index 00000000..142d6252 --- /dev/null +++ b/redux/slices/admin.ts @@ -0,0 +1,20 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +const initialState = false; + +export const adminSlice = createSlice({ + name: "admin", + initialState, + reducers: { + setAdmin: (state, action: PayloadAction) => { + const admin = action.payload; + localStorage.admin = admin; + + return (state = admin); + }, + }, +}); + +export const { setAdmin } = adminSlice.actions; + +export default adminSlice.reducer; diff --git a/redux/store.ts b/redux/store.ts index 8a515c98..a34c3af5 100644 --- a/redux/store.ts +++ b/redux/store.ts @@ -1,5 +1,6 @@ import { configureStore } from "@reduxjs/toolkit"; +import adminSlice from "./slices/admin"; import paginationSlice from "./slices/pagination"; import selectedTagsSlice from "./slices/selectedTags"; import themeSlice from "./slices/theme"; @@ -9,6 +10,7 @@ const store = configureStore({ selectedTags: selectedTagsSlice, pagination: paginationSlice, theme: themeSlice, + admin: adminSlice, }, }); diff --git a/styles/dark-theme.scss b/styles/dark-theme.scss index f578b3c3..2df5b6ec 100644 --- a/styles/dark-theme.scss +++ b/styles/dark-theme.scss @@ -40,19 +40,6 @@ filter: brightness(0) invert(1); } - .intro-section { - background: #070809; - color: #f2f5f7; - } - - .intro-text h1 { - color: #fff; - } - - .intro-avatar .name { - color: #fff; - } - .filter-tags-list li { background: #33393f; color: #f2f5f7; diff --git a/styles/main.scss b/styles/main.scss index e1329547..c0771032 100644 --- a/styles/main.scss +++ b/styles/main.scss @@ -427,75 +427,6 @@ header { background: #fe6c0a; } -/*intro-section =================================================*/ - -.intro-section { - background: #f8fafc; - padding: 35px 0; - color: #707070; -} - -.intro-content { - display: -ms-flexbox; - display: flex; - -ms-flex-align: center; - align-items: center; -} - -.intro-avatar { - -ms-flex-negative: 0; - flex-shrink: 0; - margin-left: auto; - text-align: center; - max-width: 136px; - -ms-flex-order: 2; - order: 2; -} - -.intro-avatar .image { - width: 90px; - height: 90px; - border-radius: 50%; - overflow: hidden; - margin: 0 auto 16px; -} - -.intro-avatar .image img { - display: block; - height: 100%; - width: 100%; - object-fit: cover; -} - -.intro-avatar .name { - color: #3a3a3a; - font-size: 16px; - font-weight: 700; - text-transform: uppercase; - margin-bottom: 4px; -} - -.intro-avatar .job { - line-height: 1.5; - font-size: 14px; -} - -.intro-text { - padding-right: 70px; -} - -.intro-text h1 { - color: #3a3a3a; - line-height: 1.28; - font-weight: bold; - font-size: 50px; - margin-bottom: 17px; -} - -.intro-text p { - line-height: 1.69; -} - /*simple-section =================================================*/ .simple-section { @@ -1348,7 +1279,7 @@ blockquote p:last-child { top: -43px; } -.posts-list-block-draft-or-future .post-img:after { +.post-img:after { border: none; height: 50%; } diff --git a/styles/responsive.scss b/styles/responsive.scss index 5b88e197..c20183ea 100644 --- a/styles/responsive.scss +++ b/styles/responsive.scss @@ -26,10 +26,6 @@ } @media (max-width: 1259px) { - .intro-text { - padding-right: 40px; - } - .posts-list-block .content { padding: 25px 20px; } @@ -85,14 +81,6 @@ max-width: 150px; } - .intro-text { - padding-right: 35px; - } - - .intro-text h1 { - font-size: 40px; - } - .filter-tags-list { margin: 0 -5px 16px; } @@ -223,25 +211,10 @@ width: 6px; } - .intro-content { - text-align: center; - display: block; - } - - .intro-avatar { - width: 100%; - max-width: 100%; - margin-bottom: 20px; - } - .posts-list li { width: calc(50% - 10px); } - .intro-text { - padding-right: 0; - } - .filter-tags-list { -ms-flex-pack: center; justify-content: center; @@ -568,11 +541,6 @@ } @media (max-width: 480px) { - .intro-text h1 { - font-size: 30px; - margin-bottom: 13px; - } - .filter-tags-list { margin: 0 -4px 15px; font-size: 13px; diff --git a/yarn.lock b/yarn.lock index d2e1d977..8e01f845 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1074,7 +1074,7 @@ convert-source-map@^1.5.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== - + core-js-pure@^3.20.2: version "3.23.3" resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.23.3.tgz"