diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..88249539f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +.git +.gitignore \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..18c914718 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..57a080ca8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,107 @@ +# Contributing + +We would ❤️ you to contribute to Headless Hashnode Starter kit and help make it better! We want contributing to Hashnode to be fun, enjoyable, and educational for anyone and everyone. All contributions are welcome, including issues, integrations, updates and tweaks. + +## Contribution Guidelines + +### General Contributions + +While we welcome a wide range of contributions, it's important to align with the project's goals. Here are the types of contributions we value the most: + +- **New Features**: Adding new capabilities or tools that enhance the functionality of the starter kit. +- **Bug Fixes**: Resolving existing issues to improve stability and performance. +- **Integrations**: Building connectors or tools that integrate with other services or platforms. +- **Improvements**: Enhancements that add value without altering the core look and feel in a subjective manner. + +**Note: We generally do not accept pull requests that solely make cosmetic changes, such as altering font weight, padding, text decoration, etc unless they solve an existing issue or add a new generic feature. This is because we believe that cosmetic changes are subjective and users might have different preferences.** + +If your contribution falls into the above categories, we encourage you to submit a pull request! + +### Theme Contributions + +While we appreciate the creativity in creating new themes, we want to keep the starter kit streamlined and focused on its core themes: Personal, Enterprise, and Hashnode. Therefore, we do not accept direct PRs for adding new themes to the main repository. Instead, we encourage you to: + +1. **Host your theme in a separate repository**. +2. **Deploy your theme to a live demo site**. +3. **Open an issue on our GitHub repository and provide the links in the description.**. + +We will showcase these themes under the `Community Themes` section in our README, allowing others to discover and use them. + +## How to Start? + +If you are worried or don’t know where to start, you can checkout open issues or add new issues and comment your interest and a maintainer can guide you. Alternatively, you can send your questions to anyone from the [Hashnode team on Discord](https://hshno.de/discord). + +## Submit a Pull Request 🚀 + +Branch naming convention is as following + +`TYPE-DESCRIPTION` + +example: + +``` +feat-adds-profile-section +``` + +When `TYPE` can be: + +- **feat** - a new feature +- **fix** - a bug fix +- **refactor** - code change that neither fixes a bug nor adds a feature + +**All PRs must include a commit message with the description of the changes made!** + +For the initial start, fork the project and use git clone command to download the repository to your computer. A standard procedure for working on an issue would be to: + +1. `git pull`, before creating a new branch, pull the changes from upstream. Your main branch needs to be up to date. + +``` +$ git pull +``` + +2. Create a new branch from `main` like: `feat-adds-profile-section`.
+ +``` +$ git checkout -b [name_of_your_new_branch] +``` + +3. Work - commit - repeat (make sure you're on the correct branch!) + +4. Push changes to GitHub. + +``` +$ git push origin [name_of_your_new_branch] +``` + +5. Submit your changes for review + If you go to your repository on GitHub, you'll see a `Compare & pull request` button. Click on that button. +6. Start a Pull Request + Now submit the pull request and click on `Create pull request`. +7. Get a code review approval/reject. +8. After approval, merge your PR. +9. GitHub will automatically delete the branch after the merge is done. (they can still be restored). + +## Setup From Source + +To set up a working **development environment**, just fork the project git repository and install the necessary packages with the `pnpm install` command. + +> If you just want to get started with Headless Hashnode for day-to-day use and not as a contributor, you can refer to the [guide](https://hashnode.com/headless) or the [README](README.md) file. + +```bash +git clone git@github.com:[YOUR_FORK_HERE]/starter-kit.git + +cd starter-kit + +pnpm install + +pnpm run dev +``` + + Note- Make sure to run `pnpm run dev` in the correct folder. You can find more instructions about it in the [README](https://github.com/Hashnode/starter-kit/?tab=readme-ov-file#running-locally) + +## Resources + +To stay updated with latest updates of Hashnode, you can follow: + +- [Changelog](https://hashnode.com/changelog) +- [Hashnode Discord Server](https://hshno.de/discord) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..f715addb1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM node:18-alpine + +COPY . /app + +# Set the working directory +WORKDIR /app + +# Install pnpm +RUN npm install -g pnpm + +# By default, use the enterprise theme +ARG THEME=enterprise + +WORKDIR /app/packages/blog-starter-kit/themes/${THEME} +RUN cp .env.example .env.local +RUN pnpm install --frozen-lockfile + +RUN pnpm build + +# Expose the port Next.js runs on +EXPOSE 3000 + +# Run the Next.js start script +CMD ["pnpm", "start"] diff --git a/README.md b/README.md index 593a5cffe..e1c071990 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ - +

@@ -23,23 +23,36 @@ Blog Starter Kit lets you instantly deploy a Next.js and Tailwind powered frontend for your Hashnode blog. It consumes [Hashnode's Public APIs](https://apidocs.hashnode.com), and gives you a fully customizable blog that can be deployed anywhere, including a subpath of a custom domain. Combined with [Hashnode's headless mode](https://hashnode.com/headless), it unlocks entirely new possibilities. You can now use Hashnode's [world class editor](https://hashnode.com/neptune) and dashboard to author content and collaborate. And use blog starter kit to customize the frontend to your liking. -# Live Demos: +# Live Demos -- [Personal Blog](https://sandeep.dev/blog) -- [Enterprise Blog](https://demo.hashnode.com/engineering) +Please note: The themes showcased in these demos have been heavily customized. + +- [Personal theme](https://starter-diopgk410-hashnode-prod.vercel.app/blog) +- [Enterprise theme](https://hashnode.com/blog) +- [Hashnode theme](https://datazip.io/blog?utm_source=hashnode&utm_medium=github&utm_campaign=promotional) + +# Example company blogs built with Headless + +- [MindsDB](https://mindsdb.com/blog) +- [Pangea Cloud](https://pangea.cloud/blog) +- [Outerbase](https://outerbase.com/blog) +- [Fern](https://blog.buildwithfern.com/) +- [Fix](https://fix.tt/blog) ## How to deploy ### Step 1 -The recommended approach is depoying to Vercel. If you don't have an account already, sign up for a free plan. +The recommended approach is deploying to Vercel. If you don't have an account already, you can sign up for a free plan. - Fork this repo - Create a new project on Vercel and connect this repo -- It's a monorepo. So, choose the either `packages/blog-starter-kit/themes/enterprise` or `packages/blog-starter-kit/themes/personal` as the root directory while importing on Vercel. - ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1695083263935/T5bByLxZT.png?w=500&h=800&auto=format) +- It's a monorepo, So choose either `packages/blog-starter-kit/themes/enterprise`, `packages/blog-starter-kit/themes/hashnode`, or `packages/blog-starter-kit/themes/personal` as the root directory while importing on Vercel. + + ![selecting the directory to deploy a monorepo](https://cdn.hashnode.com/res/hashnode/image/upload/v1698839884060/O8OoBML5v.PNG?auto=format) + - Choose `Next.js` as framework preset (just above Root Directory setting). -- Set the following env vars +- Set the following environment variables ``` NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT=https://gql.hashnode.com @@ -56,7 +69,7 @@ Follow the steps below if you would like to install your blog under a custom dom #### Vercel -If your main project is deployed on Vercel, add the following rewrite to `next.config.js`: +If your main project is deployed on Vercel, add the following rewrite to `next.config.js` file: ``` async rewrites() { @@ -75,6 +88,11 @@ async rewrites() { Once you deploy your project, the subpath installation should work successfully. +> Note:
+> +> - If you are updating your environment variables in Vercel, make sure to manually redeploy to see the changes. +> - Your main project refers to the project you'll have to have your blog subpath on; for example, if you'd like your blog subpath to be `https://portfolio.com/blog`, then your main project would be `https://portfolio.com`. This means that the rewrites function should be added to the codebase of the main project, not the starter kit codebase. + #### Cloudflare In case you are using Cloudflare in proxy mode (orange cloud on), you can deploy the following worker script and map it to `yourdomain.com/*`: @@ -96,7 +114,7 @@ async function handleRequest(request) { if (url.pathname.startsWith(subpath)) { // Proxy blog requests - return proxyBlog(request) + return proxyBlog(request) } else { // Passthrough everything else return fetch(request) @@ -113,25 +131,31 @@ async function proxyBlog(request) { } ``` -Be sure to replace the values of `subpath` and `blogBaseUrl` in the above snippet. This way cloudflare will proxy all the requests starting with `youdomain.com/blog` to your headless blog, and other requests will hit your origin as usual. +After the above step is done, follow these steps to add the worker route: + +- Go to `Websites` then click on your website and select `Worker Routes` from the left pane. +- Click on `Add route` and add `https://yourdomain/*` , then select the worker you just added above and click `Save`. +- Go to `https://yourdomain/yoursubpath` and now you should be able to see your blogs. + +Make sure to replace the values of `subpath` and `blogBaseUrl` in the above code snippet. This way, Cloudflare will proxy all the requests starting with `yourdomain.com/blog` to your headless blog, and other requests will hit your origin as usual. -If your main domain is hosted elsewhere, you need to involve engineers from your team to create above rewrites. +If your main domain is hosted elsewhere, you need to involve engineers from your team to create the above rewrites. ### Step 3 -Now that you have deployed the starter kit on your own domain, you need to tell Hashnode not to generate a UI for your blog. You can do that by visiting your blog dashboard -> advanced tab. Scroll down and locate the section "use Hashnode as a headless CMS". Enable it and enter your blog base URL. +Now that you have deployed the starter kit on your own domain, you need to tell Hashnode not to generate a UI for your blog. You can do that by visiting your blog dashboard -> domain -> Headless mode. Enable headless mode and enter your blog base URL. -![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697486863293/zMMctLjRZ.png?auto=format) +enable headless mode -After enabling, enter your blog URL like the following and save. +After enabling, enter your blog URL as shown below and save. -![](https://cdn.hashnode.com/res/hashnode/image/upload/v1697487035077/1sIyw_0v1.png?auto=format) +blog base url Congrats 🎉! Hashnode will now treat your blog as a headless blog and send readers directly to the origin. ## Running Locally -- cd `packages/blog-starter-kit/themes/enterprise` or `packages/blog-starter-kit/themes/personal` +- cd into either `packages/blog-starter-kit/themes/enterprise`, or `packages/blog-starter-kit/themes/hashnode` or `packages/blog-starter-kit/themes/personal` - Copy `.env.example` to `.env.local` - `pnpm install` - `pnpm dev` @@ -145,12 +169,30 @@ If you prefer to build your frontend from scratch, you can use our public GraphQ - [Docs](https://apidocs.hashnode.com) - [GraphQL Playground](https://gql.hashnode.com) +## Pricing + +**For individual devs:** Hashnode's Headless CMS is free for individual bloggers! Grab our starter kit and start building your blog – no license is needed. + +**For teams and enterprises:** Access to headless mode, multiple team members, real-time collaboration, AI, and enterprise reliability. [Request access and get a quote.](https://forms.hashnode.com/headless-hashnode-teams) We will be in touch within the next 24hrs to get you onboarded. + ## Demo Videos [![Headless Hashnode Demo — With Blog Starter Kit (Deployed to Vercel)](https://cdn.hashnode.com/res/hashnode/image/upload/v1697541065189/5ct0eFWIu.png?auto=format&w=500)](https://youtu.be/5Yuxoqohvrk) [![Customizing Hashnode Blog Starter Kit using TailwindCSS — Headless Hashnode Demo](https://cdn.hashnode.com/res/hashnode/image/upload/v1697540919799/MWVa0aD78.png?auto=format&w=500)](https://youtu.be/oH8QG8E0Txk) +## Community Themes + +In addition to our core themes, the community has developed a variety of themes to customize your blog. Check out these themes and explore their unique designs: + +| Theme Name | Demo Link | Codebase Link | +| ------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| **Newsletter Theme** | [View Demo](https://starter-kit1-6yte.vercel.app/) | [GitHub Repository](https://github.com/masterismail/starter-kit1/tree/main) | +| **Minimalist (Ikigai) Theme** | [View Demo](https://pravinreacts.vercel.app/) | [GitHub Repository](https://github.com/pravintargaryen/starter-kit/tree/main) | +| **Floaty Portfolio/Blog Theme** | [View Demo](https://floaty-hashnode-headless.vercel.app/) | [GitHub Repository](https://github.com/iammarmirza/floaty-hashnode-headless/tree/main) | + +> Note: These themes are maintained by the community and are not part of the official starter kit. + ## Found an issue? If you have found an issue or bug, please create an [issue](https://github.com/Hashnode/starter-kit/issues). @@ -163,6 +205,6 @@ Feel free to create an [issue](https://github.com/Hashnode/starter-kit/issues) w ## Reach out for help -You can discuss ideas, ask questions, and meet other members from the Hashnode community in our [Discord](https://discord.gg/hashnode). You can also create tickets on [our intercom](https://hashnode.com/#support) to find support. +You can discuss ideas, ask questions, and meet other members from the Hashnode community in our [Discord](https://discord.gg/qsAQfxX). You can also create tickets on [our intercom](https://hashnode.com/#support) to find support. If you like, you can also DM us on [X](https://x.com/hashnode)! diff --git a/license.md b/license.md index 8a0b4cffa..2248552c8 100644 --- a/license.md +++ b/license.md @@ -1,9 +1,9 @@ The MIT License (MIT) -Copyright (c) 2023 Hashnode. +Copyright (c) 2024 Hashnode. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/blog-starter-kit/themes/enterprise/.graphqlrc.yml b/packages/blog-starter-kit/themes/enterprise/.graphqlrc.yml index 4bd27f048..8d2918601 100644 --- a/packages/blog-starter-kit/themes/enterprise/.graphqlrc.yml +++ b/packages/blog-starter-kit/themes/enterprise/.graphqlrc.yml @@ -1,2 +1,2 @@ schema: './generated/schema.graphql' -documents: './{pages,components}/**/*.{graphql,js,ts,jsx,tsx}' +documents: './{pages,components,lib}/**/*.{graphql,js,ts,jsx,tsx}' diff --git a/packages/blog-starter-kit/themes/enterprise/README.md b/packages/blog-starter-kit/themes/enterprise/README.md index 2fbf3d715..2a64a8799 100644 --- a/packages/blog-starter-kit/themes/enterprise/README.md +++ b/packages/blog-starter-kit/themes/enterprise/README.md @@ -1,31 +1,11 @@ # A statically generated blog example using Next.js, Markdown, and TypeScript with Hashnode 💫 -This is the existing [blog-starter](https://github.com/vercel/next.js/tree/canary/examples/blog-starter) plus TypeScript. -wired with [Hashnode](https://hashnode.com). +This is the existing [blog-starter](https://github.com/vercel/next.js/tree/canary/examples/blog-starter) plus TypeScript, wired with [Hashnode](https://hashnode.com). -We've used [Hashnode API's](https://apidocs.hashnode.com) and integrated them with this blog starter kit. +We've used [Hashnode APIs](https://apidocs.hashnode.com) and integrated them with this blog starter kit. ## Want to have your own? -Fork it and change the environment variable `NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST` to your host (engineering.hashnode.dev is the host in the example) and deploy it to Vercel. -That's it! You now have your own frontend. You can still use Hashnode for writing your Articles. +Fork it and change the environment variable `NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST` to your host (engineering.hashnode.dev is the host in the example) and deploy it to Vercel. That's it! You now have your own frontend. You can still use Hashnode for writing your Articles. -## Demo - -[https://next-blog-starter.vercel.app/](https://next-blog-starter.vercel.app/) - -## Deploy your own - -Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) or preview live with [StackBlitz](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/blog-starter) - -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/next.js/tree/canary/examples/blog-starter&project-name=blog-starter&repository-name=blog-starter) - -# Notes - -`blog-starter` uses [Tailwind CSS](https://tailwindcss.com) [(v3.0)](https://tailwindcss.com/blog/tailwindcss-v3). - -## To-Do - -- [ ] Pagination -- [ ] Vercel Deploy Button -- [ ] Submit as Template +Demo of the `enterprise` theme: [https://demo.hashnode.com/engineering](https://demo.hashnode.com/engineering). diff --git a/packages/blog-starter-kit/themes/enterprise/components/about-author.tsx b/packages/blog-starter-kit/themes/enterprise/components/about-author.tsx new file mode 100644 index 000000000..c5fc4dd24 --- /dev/null +++ b/packages/blog-starter-kit/themes/enterprise/components/about-author.tsx @@ -0,0 +1,36 @@ +import PostAuthorInfo from './post-author-info'; +import { useAppContext } from './contexts/appContext'; +import { PostFullFragment } from '../generated/graphql'; + +function AboutAuthor() { + const { post: _post } = useAppContext(); + const post = _post as unknown as PostFullFragment; + const { publication, author } = post; + let coAuthors = post.coAuthors || []; + + const allAuthors = publication?.isTeam ? [author, ...coAuthors] : [author]; + + return ( +
+
+
+

+ Written by +

+
+ {allAuthors.map((_author) => { + return ( + + ); + })} +
+
+
+
+ ); +} + +export default AboutAuthor; \ No newline at end of file diff --git a/packages/blog-starter-kit/themes/enterprise/components/analytics.tsx b/packages/blog-starter-kit/themes/enterprise/components/analytics.tsx index 75db28156..2dcdb1969 100644 --- a/packages/blog-starter-kit/themes/enterprise/components/analytics.tsx +++ b/packages/blog-starter-kit/themes/enterprise/components/analytics.tsx @@ -1,24 +1,14 @@ import Cookies from 'js-cookie'; import { useEffect } from 'react'; import { v4 as uuidv4 } from 'uuid'; -import { useAppContext } from './contexts/appContext'; +import { useAppContext } from './contexts/appContext'; const GA_TRACKING_ID = 'G-72XG3F8LNJ'; // This is Hashnode's GA tracking ID const isProd = process.env.NEXT_PUBLIC_MODE === 'production'; const BASE_PATH = process.env.NEXT_PUBLIC_BASE_URL || ''; export const Analytics = () => { - const { publication, post } = useAppContext(); - - useEffect(() => { - if (!isProd) return; - - _sendPageViewsToHashnodeGoogleAnalytics(); - _sendViewsToHashnodeInternalAnalytics(); - _sendViewsToHashnodeAnalyticsDashboard(); - }, []); - - if (!isProd) return null; + const { publication, post, series, page } = useAppContext(); const _sendPageViewsToHashnodeGoogleAnalytics = () => { // @ts-ignore @@ -62,81 +52,90 @@ export const Analytics = () => { }); }; - const _sendViewsToHashnodeAnalyticsDashboard = async () => { - const LOCATION = window.location; - const NAVIGATOR = window.navigator; - const currentFullURL = - LOCATION.protocol + - '//' + - LOCATION.hostname + - LOCATION.pathname + - LOCATION.search + - LOCATION.hash; - - const query = new URL(currentFullURL).searchParams; - - const utm_id = query.get('utm_id'); - const utm_campaign = query.get('utm_campaign'); - const utm_source = query.get('utm_source'); - const utm_medium = query.get('utm_medium'); - const utm_term = query.get('utm_term'); - const utm_content = query.get('utm_content'); - - let referrer = document.referrer || ''; - if (referrer.indexOf(window.location.hostname) !== -1) { - referrer = ''; - } + function _sendViewsToAdvancedAnalyticsDashboard() { + const publicationId = publication.id; + const postId = post && post.id; + const seriesId = series?.id || post?.series?.id; + const staticPageId = page && page.id; const data = { - publicationId: publication.id, - postId: post && post.id, - timestamp: Date.now(), - url: currentFullURL, - referrer: referrer, - title: document.title, - charset: document.characterSet || document.charset, - lang: NAVIGATOR.language, - userAgent: NAVIGATOR.userAgent, - historyLength: window.history.length, - timezoneOffset: new Date().getTimezoneOffset(), - utm_id, - utm_campaign, - utm_source, - utm_medium, - utm_term, - utm_content, + publicationId, + postId, + seriesId, + staticPageId, }; - // send to Umami powered advanced Hashnode analytics - if (publication.integrations?.umamiWebsiteUUID) { - await fetch(`${BASE_PATH}/api/collect`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - payload: { - website: publication.integrations.umamiWebsiteUUID, - url: window.location.pathname, - referrer: referrer, - hostname: window.location.hostname, - language: NAVIGATOR.language, - screen: `${window.screen.width}x${window.screen.height}`, - }, - type: 'pageview', + if (!publicationId) { + console.warn('Publication ID is missing; could not send analytics.'); + return; + } + + const isBrowser = typeof window !== 'undefined'; + if (!isBrowser) { + return; + } + + const isLocalhost = window.location.hostname === 'localhost'; + if (isLocalhost) { + console.warn( + 'Analytics API call is skipped because you are running on localhost; data:', + data, + ); + return; + } + + const event = { + // timestamp will be added in API + payload: { + publicationId, + postId: postId || null, + seriesId: seriesId || null, + pageId: staticPageId || null, + url: window.location.href, + referrer: document.referrer || null, + language: navigator.language || null, + screen: `${window.screen.width}x${window.screen.height}`, + }, + type: 'pageview', + }; + + const blob = new Blob( + [ + JSON.stringify({ + events: [event], }), + ], + { + type: 'application/json; charset=UTF-8', + }, + ); + + let hasSentBeacon = false; + try { + if (navigator.sendBeacon) { + hasSentBeacon = navigator.sendBeacon(`${BASE_PATH}/api/analytics`, blob); + } + } catch (error) { + // do nothing; in case there is an error we fall back to fetch + } + + if (!hasSentBeacon) { + fetch(`${BASE_PATH}/api/analytics`, { + method: 'POST', + body: blob, + credentials: 'omit', + keepalive: true, }); } + } - // For Hashnode Blog Dashboard Analytics - fetch(`${BASE_PATH}/ping/view`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ data }), - }); - }; + useEffect(() => { + if (!isProd) return; + + _sendPageViewsToHashnodeGoogleAnalytics(); + _sendViewsToHashnodeInternalAnalytics(); + _sendViewsToAdvancedAnalyticsDashboard(); + }, []); return null; }; diff --git a/packages/blog-starter-kit/themes/enterprise/components/avatar.tsx b/packages/blog-starter-kit/themes/enterprise/components/avatar.tsx index 39e21f8ab..a52ed1426 100644 --- a/packages/blog-starter-kit/themes/enterprise/components/avatar.tsx +++ b/packages/blog-starter-kit/themes/enterprise/components/avatar.tsx @@ -1,7 +1,5 @@ import { resizeImage } from '@starter-kit/utils/image'; - -const DEFAULT_AVATAR = - 'https://cdn.hashnode.com/res/hashnode/image/upload/v1659089761812/fsOct5gl6.png'; +import { DEFAULT_AVATAR } from '../utils/const'; type Props = { username: string; diff --git a/packages/blog-starter-kit/themes/enterprise/components/co-authors-modal.tsx b/packages/blog-starter-kit/themes/enterprise/components/co-authors-modal.tsx new file mode 100644 index 000000000..9f0c682ea --- /dev/null +++ b/packages/blog-starter-kit/themes/enterprise/components/co-authors-modal.tsx @@ -0,0 +1,85 @@ +import { Button } from './button'; +import CloseSVG from './icons/svgs/CloseSVG'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import CustomScrollArea from './scroll-area'; +import { DEFAULT_AVATAR } from '../utils/const'; +import { ResizableImage } from './resizable-image'; +import { useAppContext } from './contexts/appContext'; +import { PostFullFragment } from '../generated/graphql'; + +type CoAuthorsModalProps = { + closeModal: () => void; +}; + +const AuthorCard = ({ author }: { author: PostFullFragment['author']; }) => { + + return ( +
+
+
+ + + +
+ + {author.name} + +
+
+
+
+ ); +}; + +export default function CoAuthorsModal({ closeModal }: CoAuthorsModalProps) { + const { post } = useAppContext(); + const authors = [post?.author, ...(post?.coAuthors || [])]; + + return ( + + + + + + Authors in this article + + + + )} + {!post.coAuthors?.length && ( + + {post.author.name} + + )} + {post.coAuthors && post.coAuthors.length > 0 && ( + + )} + +
+ · + + {readTimeInMinutes && ·} + +
{coverImage && (
@@ -36,6 +101,9 @@ export const PostHeader = ({ title, coverImage, date, author }: Props) => { />
)} + {isCoAuthorModalVisible && ( + + )} ); }; diff --git a/packages/blog-starter-kit/themes/enterprise/components/post-preview.tsx b/packages/blog-starter-kit/themes/enterprise/components/post-preview.tsx index 75fa20bc1..c5b290e9b 100644 --- a/packages/blog-starter-kit/themes/enterprise/components/post-preview.tsx +++ b/packages/blog-starter-kit/themes/enterprise/components/post-preview.tsx @@ -1,6 +1,7 @@ import { resizeImage } from '@starter-kit/utils/image'; import Link from 'next/link'; import { User } from '../generated/graphql'; +import { DEFAULT_COVER } from '../utils/const'; import { CoverImage } from './cover-image'; import { DateFormatter } from './date-formatter'; @@ -15,9 +16,6 @@ type Props = { slug: string; }; -const DEFAULT_COVER = - 'https://cdn.hashnode.com/res/hashnode/image/upload/v1683525272978/MB5H_kgOC.png?auto=format'; - export const PostPreview = ({ title, coverImage, date, excerpt, slug }: Props) => { const postURL = `/${slug}`; diff --git a/packages/blog-starter-kit/themes/enterprise/components/post-read-time-in-minutes.tsx b/packages/blog-starter-kit/themes/enterprise/components/post-read-time-in-minutes.tsx new file mode 100644 index 000000000..426f36ee3 --- /dev/null +++ b/packages/blog-starter-kit/themes/enterprise/components/post-read-time-in-minutes.tsx @@ -0,0 +1,14 @@ +import BookOpenSVG from './icons/svgs/BookOpenSVG'; + +type Props = { readTimeInMinutes: number }; + +export const ReadTimeInMinutes = ({ readTimeInMinutes }: Props) => { + return ( + <> +

+ + {readTimeInMinutes} min read +

+ + ); +}; diff --git a/packages/blog-starter-kit/themes/enterprise/components/profile-image.js b/packages/blog-starter-kit/themes/enterprise/components/profile-image.js new file mode 100644 index 000000000..08d078de0 --- /dev/null +++ b/packages/blog-starter-kit/themes/enterprise/components/profile-image.js @@ -0,0 +1,56 @@ +import React from 'react'; +import { twMerge } from 'tailwind-merge'; +import Image from 'next/legacy/image'; + +import { resizeImage } from '@starter-kit/utils/image'; +import { DEFAULT_AVATAR } from '../utils/const'; + +export default class ProfileImage extends React.Component { + componentDidMount() { + if (!this.props.user) { + return; + } + if (this.props.user.isDeactivated) { + return; + } + } + + render() { + const user = this.props.user; + const blogURL = this.props.blogURL; + return ( + { + this.profileImage = c; + }} + className={`relative block h-full w-full`} + > + {user + + ); + } +} diff --git a/packages/blog-starter-kit/themes/enterprise/components/progressive-image.tsx b/packages/blog-starter-kit/themes/enterprise/components/progressive-image.tsx new file mode 100644 index 000000000..59da06679 --- /dev/null +++ b/packages/blog-starter-kit/themes/enterprise/components/progressive-image.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { resizeImage } from '@starter-kit/utils/image'; + +import { DEFAULT_AVATAR } from '../utils/const'; +import { twMerge } from 'tailwind-merge'; + +/** + * Progressive Image Component which loads low resolution version image before loading original + * @param {string} options.src Image source + * @param {string} options.alt Image alt text + * @param {string} options.className Classname string + * @param {...[type]} options.restOfProps Rest of the props passed to the child + */ +class ProgressiveImage extends React.Component<{ + resize: any; + src: string; + alt: string; + className: string; + css: string; +}> { + image: HTMLImageElement | null = null; + + componentDidMount() { + if (!(window as any).lazySizes && this.image) { + this.image.setAttribute('src', this.image.getAttribute('data-src') || ''); + } + } + + // TODO: Improve type + replaceBadImage = (e: any) => { + // eslint-disable-next-line react/destructuring-assignment + if (this.props.resize && this.props.resize.c !== 'face') { + return; + } + e.target.onerror = null; + e.target.src = DEFAULT_AVATAR; + }; + + render() { + const { src, alt, className, resize = {}, ...restOfProps } = this.props; + + if (!src || src.trim().length === 0) return null; + + const resizedImage = resizeImage(src, resize); + + return ( + (this.image = c || null)} + data-src={resizedImage} + width={resize.w} + height={resize.h} + onError={this.replaceBadImage} + alt={alt} + className={twMerge('lazyload block w-full', className)} + {...restOfProps} + /> + ); + } +} + +export default ProgressiveImage; + +export { ProgressiveImage }; \ No newline at end of file diff --git a/packages/blog-starter-kit/themes/enterprise/components/publication-logo.tsx b/packages/blog-starter-kit/themes/enterprise/components/publication-logo.tsx new file mode 100644 index 000000000..11dcde8e9 --- /dev/null +++ b/packages/blog-starter-kit/themes/enterprise/components/publication-logo.tsx @@ -0,0 +1,45 @@ +import { resizeImage } from '@starter-kit/utils/image'; +import Link from 'next/link'; +import { useAppContext } from './contexts/appContext'; +import { PublicationFragment } from '../generated/graphql'; + +const getPublicationLogo = (publication: PublicationFragment, isSidebar?: boolean) => { + if (isSidebar) { + return publication.preferences.logo; // Always display light mode logo in sidebar + } + return publication.preferences.darkMode?.logo || publication.preferences.logo; +} + +export const PublicationLogo = ({ isSidebar }: { isSidebar?: boolean }) => { + const { publication } = useAppContext(); + const PUBLICATION_LOGO = getPublicationLogo(publication, isSidebar); + + return ( +

+ + {PUBLICATION_LOGO ? ( + <> + {publication.title} + Blog + + ) : ( + + {publication.title} + + )} + +

+ ); +}; diff --git a/packages/blog-starter-kit/themes/enterprise/components/resizable-image.js b/packages/blog-starter-kit/themes/enterprise/components/resizable-image.js new file mode 100644 index 000000000..a3016bb4c --- /dev/null +++ b/packages/blog-starter-kit/themes/enterprise/components/resizable-image.js @@ -0,0 +1,12 @@ +import { ProgressiveImage } from './progressive-image'; + +function ResizableImage(props) { + const { src, alt, resize, className, ...restOfTheProps } = props; + + return ( + + ); +} + +export default ResizableImage; +export { ResizableImage }; \ No newline at end of file diff --git a/packages/blog-starter-kit/themes/enterprise/components/scroll-area.tsx b/packages/blog-starter-kit/themes/enterprise/components/scroll-area.tsx new file mode 100644 index 000000000..c4d1ddb57 --- /dev/null +++ b/packages/blog-starter-kit/themes/enterprise/components/scroll-area.tsx @@ -0,0 +1,78 @@ +import { PropsWithChildren } from 'react'; +import * as ScrollArea from '@radix-ui/react-scroll-area'; + +const scrollbarStyles = ` +.ScrollAreaRoot { + width: 100%; + --scrollbar-size: 2px; + overflow: auto; + } + + .ScrollAreaViewport { + width: 100%; + height: 100%; + border-radius: inherit; + } + + .ScrollAreaScrollbar { + display: flex; + /* ensures no selection */ + user-select: none; + /* disable browser handling of all panning and zooming gestures on touch devices */ + touch-action: none; + padding: 5px; + transition: background 160ms ease-out; + } + .ScrollAreaScrollbar[data-orientation='vertical'] { + width: 5px; + } + .ScrollAreaScrollbar[data-orientation='horizontal'] { + flex-direction: column; + height: 5px; + } + + .ScrollAreaThumb { + flex: 1; + border-radius: 20px; + position: relative; + } + .ScrollAreaThumb::before { + background: #E2E8F0; + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 100%; + height: 100%; + border-radius: 20px; + min-width: 9px; + min-height: 9px; + } + .dark .ScrollAreaThumb::before { + background: #475569; + } +`; + +interface ScrollAreaProps extends PropsWithChildren {} + +const CustomScrollArea = ({ children }: ScrollAreaProps) => ( + <> + +); diff --git a/packages/blog-starter-kit/themes/hashnode/components/header-blog-search.tsx b/packages/blog-starter-kit/themes/hashnode/components/header-blog-search.tsx new file mode 100644 index 000000000..96845779c --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/header-blog-search.tsx @@ -0,0 +1,41 @@ +/* eslint-disable no-nested-ternary */ +import { useState, useRef } from 'react'; +import dynamic from 'next/dynamic'; + +import CommonHeaderIconBtn from './common-header-icon-btn'; +import { PublicationFragment } from '../generated/graphql'; +import SearchSVG from './icons/svgs/SearchSvg'; + +const PublicationSearch = dynamic(() => import('./publication-search'), { ssr: false }); + +interface Props { + publication: Pick +} + +const HeaderBlogSearch = (props: Props) => { + const { publication } = props; + + const [isSearchUIVisible, toggleSearchUIState] = useState(false); + const triggerRef = useRef(null); + + const toggleSearchUI = () => { + toggleSearchUIState(!isSearchUIVisible); + }; + + return ( + <> + {isSearchUIVisible ? ( + + ) : null} + + + + + ); +}; + +export default HeaderBlogSearch; diff --git a/packages/blog-starter-kit/themes/hashnode/components/header-left-sidebar.tsx b/packages/blog-starter-kit/themes/hashnode/components/header-left-sidebar.tsx new file mode 100644 index 000000000..cd38a5075 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/header-left-sidebar.tsx @@ -0,0 +1,43 @@ +/* eslint-disable no-nested-ternary */ +import dynamic from 'next/dynamic'; +import { useState, useRef } from 'react'; + +import { PublicationFragment } from '../generated/graphql'; +import { BarsSVG } from './icons/svgs'; +import CommonHeaderIconBtn from './common-header-icon-btn'; + +const PublicationSidebar = dynamic(() => import('./publication-sidebar'), { + ssr: false, +}); + +interface Props { + publication: Pick; +} + +const LeftSidebarButton = (props: Props) => { + const { publication } = props; + + const triggerRef = useRef(null); + const [isSidebarVisible, toggleSidebarVisibility] = useState(false); + + const toggleSidebar = () => { + toggleSidebarVisibility(!isSidebarVisible); + }; + + return ( + <> + {isSidebarVisible ? ( + + ) : null} + + + + + ); +}; + +export default LeftSidebarButton; diff --git a/packages/blog-starter-kit/themes/hashnode/components/header-tooltip.tsx b/packages/blog-starter-kit/themes/hashnode/components/header-tooltip.tsx new file mode 100644 index 000000000..68f1f9e10 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/header-tooltip.tsx @@ -0,0 +1,39 @@ +import * as RadixTooltip from '@radix-ui/react-tooltip'; +import { twJoin } from 'tailwind-merge'; + +import { useAppContext } from './contexts/appContext'; + +interface IHeaderTooltip { + tooltipClassName: string; + tooltipText: string; + children: React.ReactNode; +} + +const HeaderTooltip = (props: IHeaderTooltip) => { + const { tooltipClassName, tooltipText, children } = props; + + return ( + + + {children} + + + {tooltipText} + + + + + ); +}; + +export default HeaderTooltip; diff --git a/packages/blog-starter-kit/themes/hashnode/components/header.tsx b/packages/blog-starter-kit/themes/hashnode/components/header.tsx new file mode 100644 index 000000000..bacc0052f --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/header.tsx @@ -0,0 +1,84 @@ +import { twJoin } from 'tailwind-merge'; +import { lightOrDark } from '../utils/commonUtils'; +import { useAppContext } from './contexts/appContext'; +import { Button } from './custom-button'; +import HeaderBlogSearch from './header-blog-search'; +import HeaderLeftSidebar from './header-left-sidebar'; +import PublicationLogo from './publication-logo'; +import PublicationNavLinks from './publication-nav-links'; +import PublicationSocialLinks from './publication-social-links'; + +type Props = { + currentMenuId?: string | null; + isHome: boolean; +}; + +export const Header = (props: Props) => { + const { currentMenuId, isHome } = props; + const { publication } = useAppContext(); + + return ( +
+
+
+
+ {/* Navigation for mobile view */} +
+ +
+
+ +
+
+ +
+ +
+
+ + {/* Logo for mobile view */} +
+ +
+ +
+ {/* Desktop */} +
+ +
+ {/* Mobile view */} +
+ +
+
+ +
+ +
+
+
+ ); +}; diff --git a/packages/blog-starter-kit/themes/hashnode/components/hn-button.tsx b/packages/blog-starter-kit/themes/hashnode/components/hn-button.tsx new file mode 100644 index 000000000..902093e40 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/hn-button.tsx @@ -0,0 +1,137 @@ +import React from 'react'; +import { twJoin, twMerge } from 'tailwind-merge'; + +type ButtonProps = { + type?: 'button' | 'submit' | 'reset'; + variant?: 'primary' | 'transparent' | 'primary-outline' | 'transparent-outline'; + active?: boolean; + disabled?: boolean; + size?: 'small' | 'big'; + children: React.ReactChild | React.ReactChild[]; +} & React.ButtonHTMLAttributes; + +type AnchorProps = { + as?: null | 'a'; // improve this + href?: string; // improve this +} & React.AnchorHTMLAttributes; + +type ButtonAtomProps = ButtonProps & AnchorProps; + +const buttonSizes = { + small: 'px-2 text-sm', + big: 'px-4 py-2 text-lg', +}; + +type ButtonState = 'default' | 'active' | 'disabled'; + +const buttonStyles: Record['variant'], Record> = { + transparent: { + default: twJoin( + 'rounded-full border border-transparent px-3 py-1 text-base font-medium leading-relaxed text-slate-700 dark:text-slate-200', + 'hover:bg-slate-200 disabled:opacity-50 hover:dark:bg-slate-700', + 'flex flex-row items-center focus:outline-none', // Extra added, instead of sending from parent component + ), + active: 'text-blue-600 dark:text-blue-600', + disabled: 'opacity-50 cursor-not-allowed', + }, + 'transparent-outline': { + default: twJoin( + 'rounded-full border border-slate-200 px-3 py-1 text-base font-medium leading-relaxed text-slate-700 dark:border-slate-800 dark:text-slate-200', + 'hover:bg-slate-200 disabled:opacity-50 hover:dark:bg-slate-700', + 'flex flex-row items-center focus:outline-none', // Extra added, instead of sending from parent component + ), + active: 'text-white bg-blue-600 hover:bg-blue-600 border-blue-600', + disabled: 'opacity-50 cursor-not-allowed', + }, + primary: { + default: twJoin( + 'rounded-full border border-blue-600 bg-blue-600 px-3 py-1 text-center text-base font-medium leading-relaxed text-white transition-colors duration-150', + 'hover:bg-opacity-90 hover:shadow-lg focus:outline-none disabled:opacity-50', // Extra added, instead of sending from parent component + ), + active: 'text-white bg-blue-600 hover:bg-blue-500 hover:dark:bg-blue-500', + disabled: 'opacity-50 cursor-not-allowed', + }, + 'primary-outline': { + default: twJoin( + 'rounded-full border border-blue-600 px-3 py-1 text-center text-base font-medium leading-relaxed transition-colors duration-150 dark:border-blue-500', + `flex flex-row items-center justify-center text-blue-600 focus:outline-none + dark:text-blue-500`, // Extra added, instead of sending from parent component + 'hover:bg-blue-50 disabled:opacity-50 hover:dark:bg-slate-800', + ), + active: 'text-white bg-blue-600 hover:bg-blue-500 hover:dark:bg-blue-500', + disabled: 'opacity-50 cursor-not-allowed', + }, +}; + +/** + * Button component + * @param {string} props.value Button value + * @param {string=} props.variant Button variant + * @returns {React.Reactnode} Button + */ +const Index = React.forwardRef((props: ButtonAtomProps, ref: React.Ref) => { + const { size, active, as, href, disabled, children, className, ...restOfTheProps } = props; + + // use 'primary' as a fallback if a not supported variant is passed + let { variant = 'primary' } = props; + if (!(variant in buttonStyles)) { + variant = 'primary'; + } + + const styles = buttonStyles[variant]; + + if (as === 'a') { + delete restOfTheProps.type; + return ( + + {children} + + ); + } + + return ( + + ); +}); + +Index.displayName = 'Button'; + +Index.defaultProps = { + variant: 'primary', + active: false, + disabled: false, + size: undefined, + type: 'button', + as: null, + href: '#', +}; + +export default Index; diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/index.js b/packages/blog-starter-kit/themes/hashnode/components/icons/index.js new file mode 100644 index 000000000..138a0c853 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/index.js @@ -0,0 +1 @@ +export * from './svgs'; diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/AlertSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/AlertSVG.js new file mode 100644 index 000000000..3c7542369 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/AlertSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class AlertSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ArticleSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ArticleSVG.js new file mode 100644 index 000000000..ebc182545 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ArticleSVG.js @@ -0,0 +1,17 @@ +import React from 'react'; + +export default class ArticleSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/BadgeDollarSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/BadgeDollarSVG.js new file mode 100644 index 000000000..e0ddc4bfa --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/BadgeDollarSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class BadgeDollarSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/BarsSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/BarsSVG.js new file mode 100644 index 000000000..be93272a0 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/BarsSVG.js @@ -0,0 +1,16 @@ +import React from 'react'; + +export default class BarsSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/BookOpenSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/BookOpenSVG.js new file mode 100644 index 000000000..6fb9be0fd --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/BookOpenSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class BookOpenSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChartMixedSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChartMixedSVG.js new file mode 100644 index 000000000..4fc1478c8 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChartMixedSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class ChartMixedSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/CheckSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/CheckSVG.js new file mode 100644 index 000000000..f1032ea8b --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/CheckSVG.js @@ -0,0 +1,12 @@ +import React from 'react'; + +export default class CheckSVG extends React.Component { + render() { + return ( + + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronDownSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronDownSVG.js new file mode 100644 index 000000000..fcded4c89 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronDownSVG.js @@ -0,0 +1,12 @@ +import React from 'react'; + +export default class ChevronDownSVG extends React.Component { + render() { + return ( + // + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronDownSVGV2.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronDownSVGV2.js new file mode 100644 index 000000000..0de2dd109 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronDownSVGV2.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class ChevronDownSVGV2 extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronDownSVG_16x16.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronDownSVG_16x16.js new file mode 100644 index 000000000..fb6b74580 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronDownSVG_16x16.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class ChevronDownSVG_16x16 extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronLeftSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronLeftSVG.js new file mode 100644 index 000000000..7cff0944d --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronLeftSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class ChevronLeftSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronRightSVG_16x16.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronRightSVG_16x16.js new file mode 100644 index 000000000..be1a95211 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronRightSVG_16x16.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class ChevronRightSVG16x16 extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronUpSVG_16x16.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronUpSVG_16x16.js new file mode 100644 index 000000000..271b33cce --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronUpSVG_16x16.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class ChevronUpSVG16X16 extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ClipboardSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ClipboardSVG.js new file mode 100644 index 000000000..4d5365bd7 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ClipboardSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class ClipboardSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/CloseSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/CloseSVG.js new file mode 100644 index 000000000..bc5436898 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/CloseSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class CloseSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/CommentSVGV2.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/CommentSVGV2.js new file mode 100644 index 000000000..da401c184 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/CommentSVGV2.js @@ -0,0 +1,12 @@ +export default function CommentSVGV2(props) { + return ( + + + + ); +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/EarthSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/EarthSVG.js new file mode 100644 index 000000000..cbc62e0b6 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/EarthSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class EarthSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ExternalArrowSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ExternalArrowSVG.js new file mode 100644 index 000000000..24b61272f --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ExternalArrowSVG.js @@ -0,0 +1,17 @@ +import React from 'react'; + +export default class ExternalArrowSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ExternalLinkSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ExternalLinkSVG.js new file mode 100644 index 000000000..1af007450 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ExternalLinkSVG.js @@ -0,0 +1,12 @@ +import React from 'react'; + +export default class ExternalLinkSVG extends React.Component { + render() { + return ( + + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/FacebookSVGRound.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/FacebookSVGRound.js new file mode 100644 index 000000000..bbd131d60 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/FacebookSVGRound.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class FacebookSVGRound extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/FeaturedStarV2SVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/FeaturedStarV2SVG.js new file mode 100644 index 000000000..a0d77d752 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/FeaturedStarV2SVG.js @@ -0,0 +1,86 @@ +import React from 'react'; + +export default class FeaturedStarV2SVG extends React.Component { + render() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/FileLineChartSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/FileLineChartSVG.js new file mode 100644 index 000000000..b4628281f --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/FileLineChartSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class FileLineChartSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/GithubSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/GithubSVG.js new file mode 100644 index 000000000..a99fb11f8 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/GithubSVG.js @@ -0,0 +1,12 @@ +import React from 'react'; + +export default class GithubSVG extends React.Component { + render() { + return ( + // + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HackernewsSVGV2.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HackernewsSVGV2.js new file mode 100644 index 000000000..638559697 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HackernewsSVGV2.js @@ -0,0 +1,9 @@ +const HackernewsSVGV2 = (props) => { + return ( + + + + ); +}; + +export default HackernewsSVGV2; diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HamburgerSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HamburgerSVG.js new file mode 100644 index 000000000..7256df073 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HamburgerSVG.js @@ -0,0 +1,17 @@ +import React from 'react'; + +export default class HamburgerSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HashnodeLogoIconV2.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HashnodeLogoIconV2.js new file mode 100644 index 000000000..4c7f265cb --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HashnodeLogoIconV2.js @@ -0,0 +1,15 @@ +import React from 'react'; + +export default class HashnodeLogoIconV2 extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HashnodeSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HashnodeSVG.js new file mode 100644 index 000000000..66481c421 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HashnodeSVG.js @@ -0,0 +1,20 @@ +import React from 'react'; + +export default class HashnodeSVG extends React.Component { + render() { + return ( + + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HeadphonesSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HeadphonesSVG.js new file mode 100644 index 000000000..ac06ff9b1 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HeadphonesSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class HeadphonesSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/InstagramSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/InstagramSVG.js new file mode 100644 index 000000000..750b1c484 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/InstagramSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class InstagramSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/LinkAltSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/LinkAltSVG.js new file mode 100644 index 000000000..9b6efefba --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/LinkAltSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class LinkAltSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/LinkSVGV2.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/LinkSVGV2.js new file mode 100644 index 000000000..fbec4aa19 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/LinkSVGV2.js @@ -0,0 +1,9 @@ +const LinkSVGV2 = (props) => { + return ( + + + + ); +}; + +export default LinkSVGV2; diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/LinkedInSVGV2.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/LinkedInSVGV2.js new file mode 100644 index 000000000..972933c0d --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/LinkedInSVGV2.js @@ -0,0 +1,9 @@ +const LinkedInSVGV2 = (props) => { + return ( + + + + ); +}; + +export default LinkedInSVGV2; diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/LinkedinSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/LinkedinSVG.js new file mode 100644 index 000000000..404c1cd40 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/LinkedinSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class LinkedinSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ListSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ListSVG.js new file mode 100644 index 000000000..6466fb805 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ListSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class ListSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/MastodonSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/MastodonSVG.js new file mode 100644 index 000000000..e5851d159 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/MastodonSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class MastodonSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/NewsletterPlusSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/NewsletterPlusSVG.js new file mode 100644 index 000000000..cc9933ddf --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/NewsletterPlusSVG.js @@ -0,0 +1,14 @@ +import React from 'react'; + +export default class NewsletterPlusSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/NoCommentsDarkSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/NoCommentsDarkSVG.js new file mode 100644 index 000000000..130fee1f8 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/NoCommentsDarkSVG.js @@ -0,0 +1,309 @@ +import React from 'react'; + +export default class NoCommentsDarkSVG extends React.Component { + render() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {' '} + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/NoCommentsLightSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/NoCommentsLightSVG.js new file mode 100644 index 000000000..9c3ae7e0f --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/NoCommentsLightSVG.js @@ -0,0 +1,173 @@ +import React from 'react'; + +export default class NoCommentsLightSVG extends React.Component { + render() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/PaperPlaneSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/PaperPlaneSVG.js new file mode 100644 index 000000000..2cf6b8c67 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/PaperPlaneSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class PaperPlaneSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/PencilSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/PencilSVG.js new file mode 100644 index 000000000..c0ac3f21b --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/PencilSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class PencilSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/PinSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/PinSVG.js new file mode 100644 index 000000000..a4d89fce1 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/PinSVG.js @@ -0,0 +1,17 @@ +import React from 'react'; + +export default class PinSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/PlusCircleSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/PlusCircleSVG.js new file mode 100644 index 000000000..a6295b3c0 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/PlusCircleSVG.js @@ -0,0 +1,17 @@ +import React from 'react'; + +export default class PlusCircleSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RedditSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RedditSVG.js new file mode 100644 index 000000000..a9c60a7fe --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RedditSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class RedditSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RedditSVGV2.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RedditSVGV2.js new file mode 100644 index 000000000..7347cf41a --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RedditSVGV2.js @@ -0,0 +1,9 @@ +const RedditSVGV2 = (props) => { + return ( + + + + ); +}; + +export default RedditSVGV2; diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RefreshSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RefreshSVG.js new file mode 100644 index 000000000..3cd1e4bb8 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RefreshSVG.js @@ -0,0 +1,12 @@ +import React from 'react'; + +export default class RefreshSVG extends React.Component { + render() { + return ( + + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RobotSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RobotSVG.js new file mode 100644 index 000000000..fa4a8f027 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RobotSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class RobotSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RssSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RssSVG.js new file mode 100644 index 000000000..95eefc345 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RssSVG.js @@ -0,0 +1,12 @@ +import React from 'react'; + +export default class RssSVG extends React.Component { + render() { + return ( + // + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/SearchSvg.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/SearchSvg.js new file mode 100644 index 000000000..e37e04a8f --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/SearchSvg.js @@ -0,0 +1,16 @@ +import React from 'react'; + +export default class SearchSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ShareSVGV2.tsx b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ShareSVGV2.tsx new file mode 100644 index 000000000..e053fc3f1 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ShareSVGV2.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +const ShareSVGV2 = React.forwardRef>((props, ref) => ( + + + +)); + +ShareSVGV2.displayName = 'ShareSVGV2'; + +export default ShareSVGV2; diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/TwitterXSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/TwitterXSVG.js new file mode 100644 index 000000000..204631023 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/TwitterXSVG.js @@ -0,0 +1,15 @@ +import React from 'react'; + +export default class TwitterXSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/WhatsappSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/WhatsappSVG.js new file mode 100644 index 000000000..7324ec304 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/WhatsappSVG.js @@ -0,0 +1,15 @@ +import React from 'react'; + +export default class WhatsappSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/XSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/XSVG.js new file mode 100644 index 000000000..f9f7a9c05 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/XSVG.js @@ -0,0 +1,16 @@ +import React from 'react'; + +export default class XSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/YoutubeSVG.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/YoutubeSVG.js new file mode 100644 index 000000000..72adf34b5 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/YoutubeSVG.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default class YoutubeSVG extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/index.js b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/index.js new file mode 100644 index 000000000..970e61dc2 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/icons/svgs/index.js @@ -0,0 +1,99 @@ +import ArticleSVG from './ArticleSVG'; +import ChevronDownSVG from './ChevronDownSVG'; +import ExternalArrowSVG from './ExternalArrowSVG'; +import GithubSVG from './GithubSVG'; +import HashnodeSVG from './HashnodeSVG'; +import LinkedinSVG from './LinkedinSVG'; +import NewsletterPlusSVG from './NewsletterPlusSVG'; +import PlusCircleSVG from './PlusCircleSVG'; +import RssSVG from './RssSVG'; +import XSVG from './XSVG'; +import InstagramSVG from './InstagramSVG'; +import MastodonSVG from './MastodonSVG'; +import HashnodeLogoIconV2 from './HashnodeLogoIconV2'; +import EarthSVG from './EarthSVG'; +import YoutubeSVG from './YoutubeSVG'; +import TwitterXSVG from './TwitterXSVG'; +import ChevronDownV2SVG from './ChevronDownSVGV2'; +import CheckSVG from './CheckSVG'; +import BookOpenSVG from './BookOpenSVG'; +import ChartMixedSVG from './ChartMixedSVG'; +import PinSVG from './PinSVG'; +import PaperPlaneSVG from './PaperPlaneSVG'; +import ChevronLeftSVG from './ChevronLeftSVG'; +import BarsSVG from './BarsSVG'; +import CloseSVG from './CloseSVG'; +import BadgeDollarSVG from './BadgeDollarSVG'; +import ClipboardSVG from './ClipboardSVG'; +import FeaturedStarV2SVG from './FeaturedStarV2SVG'; +import HeadphonesSVG from './HeadphonesSVG'; +import RedditSVG from './RedditSVG'; +import RobotSVG from './RobotSVG'; +import ListSVG from './ListSVG'; +import ChevronDownSVG_16x16 from './ChevronDownSVG_16x16'; +import ChevronRightSVG_16x16 from './ChevronRightSVG_16x16'; +import ChevronUpSVG_16x16 from './ChevronUpSVG_16x16'; +import LinkAltSVG from './LinkAltSVG'; +import FileLineChartSVG from './FileLineChartSVG'; +import CommentSVGV2 from './CommentSVGV2'; +import NoCommentsDarkSVG from './NoCommentsDarkSVG'; +import NoCommentsLightSVG from './NoCommentsLightSVG'; +import FacebookSVGRound from './FacebookSVGRound'; +import HackernewsSVGV2 from './HackernewsSVGV2'; +import LinkedInSVGV2 from './LinkedInSVGV2'; +import LinkSVGV2 from './LinkSVGV2'; +import RedditSVGV2 from './RedditSVGV2'; +import ShareSVGV2 from './ShareSVGV2'; +import WhatsappSVG from './WhatsappSVG'; +import AlertSVG from './AlertSVG'; + +export { + ArticleSVG, + ChevronDownSVG, + ExternalArrowSVG, + GithubSVG, + HashnodeSVG, + LinkedinSVG, + NewsletterPlusSVG, + PlusCircleSVG, + RssSVG, + XSVG, + InstagramSVG, + MastodonSVG, + HashnodeLogoIconV2, + EarthSVG, + YoutubeSVG, + TwitterXSVG, + ChevronDownV2SVG, + CheckSVG, + BookOpenSVG, + ChartMixedSVG, + PinSVG, + PaperPlaneSVG, + ChevronLeftSVG, + BarsSVG, + CloseSVG, + BadgeDollarSVG, + ClipboardSVG, + FeaturedStarV2SVG, + HeadphonesSVG, + RedditSVG, + RobotSVG, + ListSVG, + ChevronDownSVG_16x16, + ChevronUpSVG_16x16, + ChevronRightSVG_16x16, + LinkAltSVG, + FileLineChartSVG, + CommentSVGV2, + NoCommentsLightSVG, + NoCommentsDarkSVG, + FacebookSVGRound, + HackernewsSVGV2, + LinkSVGV2, + LinkedInSVGV2, + RedditSVGV2, + ShareSVGV2, + WhatsappSVG, + AlertSVG +}; diff --git a/packages/blog-starter-kit/themes/hashnode/components/integrations.tsx b/packages/blog-starter-kit/themes/hashnode/components/integrations.tsx new file mode 100644 index 000000000..e2f79e92c --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/integrations.tsx @@ -0,0 +1,154 @@ +import { useEffect } from 'react'; +import { useAppContext } from './contexts/appContext'; + +export function Integrations() { + const { publication } = useAppContext(); + const { + gaTrackingID, + fbPixelID, + hotjarSiteID, + matomoURL, + matomoSiteID, + fathomSiteID, + fathomCustomDomain, + fathomCustomDomainEnabled, + plausibleAnalyticsEnabled, + gTagManagerID, + koalaPublicKey, + msClarityID, + } = publication.integrations ?? {}; + const domainURL = new URL(publication.url).hostname; + + let fbPixel = ` + !function(f,b,e,v,n,t,s) + {if(f.fbq)return;n=f.fbq=function(){n.callMethod? + n.callMethod.apply(n,arguments):n.queue.push(arguments)}; + if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0'; + n.queue=[];t=b.createElement(e);t.async=!0;t.defer=!0; + t.src=v;s=b.getElementsByTagName(e)[0]; + s.parentNode.insertBefore(t,s)}(window,document,'script', + 'https://connect.facebook.net/en_US/fbevents.js'); + `; + + if (fbPixelID) { + fbPixel += `fbq('init', '${encodeURI(fbPixelID)}');`; + } + + const hotjarForUsers = + hotjarSiteID && + ` + (function(h,o,t,j,a,r){ + h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)}; + h._hjSettings={hjid:${encodeURI(hotjarSiteID)},hjsv:6}; + a=o.getElementsByTagName('head')[0]; + r=o.createElement('script');r.async=1;r.defer=1; + r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv; + a.appendChild(r); + })(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv='); + `; + + const matomoAnalytics = ` + var _paq = window._paq = window._paq || []; + _paq.push(['trackPageView']); + _paq.push(['enableLinkTracking']); + (function() { + var u="https://${encodeURI(matomoURL || '')}/"; + _paq.push(['setTrackerUrl', u+'matomo.php']); + _paq.push(['setSiteId', '${encodeURI(matomoSiteID || '')}']); + var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; + g.type='text/javascript'; g.async=true; g.defer=true; g.src='//cdn.matomo.cloud/${encodeURI( + matomoURL || '', + )}/matomo.js'; s.parentNode.insertBefore(g,s); + })(); + `; + + const googleTagManager = ` + (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': + new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], + j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= + 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); + })(window,document,'script','dataLayer', '${gTagManagerID}');`; + + const koalaForUsers = + koalaPublicKey && + `!function(t){if(window.ko)return;window.ko=[], + ["identify","track","removeListeners","on","off","qualify","ready"] + .forEach(function(t){ko[t]=function(){var n=[].slice.call(arguments);return n.unshift(t),ko.push(n),ko}}); + var n=document.createElement("script"); + n.async=!0,n.setAttribute("src","https://cdn.getkoala.com/v1/${encodeURI(koalaPublicKey)}/sdk.js"), + (document.body || document.head).appendChild(n)}();`; + + const msClarityForUsers = + msClarityID && + `(function(c,l,a,r,i,t,y){ + c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)}; + t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i; + y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y); + })(window, document, "clarity", "script", '${msClarityID}');`; + + useEffect(() => { + // @ts-ignore + window.gtag('config', gaTrackingID, { + transport_url: 'https://ping.hashnode.com', + first_party_collection: true, + }); + }, []); + + return ( + <> + {fbPixelID ? ( + + ) : null} + {fathomSiteID && ( + + )} + {hotjarSiteID && hotjarForUsers && ( + + )} + {matomoURL && ( + + )} + {gTagManagerID && ( + + )} + {koalaForUsers && ( + + )} + {msClarityForUsers && ( + + )} + {plausibleAnalyticsEnabled && ( + + )} + + ); +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/layout.tsx b/packages/blog-starter-kit/themes/hashnode/components/layout.tsx new file mode 100644 index 000000000..039f9e30f --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/layout.tsx @@ -0,0 +1,22 @@ +import { Analytics } from './analytics'; +import { Integrations } from './integrations'; +import { Meta } from './meta'; +import { Scripts } from './scripts'; + +type Props = { + children: React.ReactNode; +}; + +export const Layout = ({ children }: Props) => { + return ( + <> + + +
+
{children}
+
+ + + + ); +}; diff --git a/packages/blog-starter-kit/themes/hashnode/components/magazine-blog-post-preview.tsx b/packages/blog-starter-kit/themes/hashnode/components/magazine-blog-post-preview.tsx new file mode 100644 index 000000000..412642b76 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/magazine-blog-post-preview.tsx @@ -0,0 +1,113 @@ +import Link from 'next/link'; + +import CustomImage from './custom-image'; +import { BookOpenSVG, ChartMixedSVG } from './icons/svgs'; +import { getDefaultPostCoverImageUrl } from '../utils/commonUtils'; +import { blurImageDimensions } from '../utils/const/images'; +import { getBlurHash, resizeImage } from '../utils/image'; +import { kFormatter } from '../utils/image'; +import { PostThumbnailFragment, PublicationFragment } from '../generated/graphql'; + +function BlogPostPreview(props: { + post: PostThumbnailFragment; + publication: Pick; +}) { + const { post, publication } = props; + const postURL = `/${post.slug}`; + const postCoverImageURL = post.coverImage?.url ?? getDefaultPostCoverImageUrl(); + + const preload = async () => { + const nextData = document.getElementById('__NEXT_DATA__'); + if (nextData) { + const { buildId } = JSON.parse(nextData.innerHTML); + if (buildId) { + fetch(`/_next/data/${buildId}/${post.slug}.json?slug=${post.slug}`); + } + } + }; + + return ( +
+ undefined} + aria-label={`Cover photo of the article titled ${post.title}`} + className="mb-4 block w-full overflow-hidden rounded-lg border bg-slate-100 hover:opacity-90 dark:border-slate-800 dark:bg-slate-800" + > + + +

+ undefined}> + {post.title} + +

+
+
+ undefined} + > + {post.author.name} + +
+ {publication.features.readTime.isEnabled && post.readTimeInMinutes ? ( + <> +

+ undefined} + > + + {post.readTimeInMinutes} min read + +

+ + ) : null} + {post.readTimeInMinutes && Number(post.views) > 0 && publication.features.viewCount.isEnabled ? ( +

·

+ ) : null} + {Number(post.views) > 0 && publication.features.viewCount.isEnabled ? ( +

+ undefined} + > + + {kFormatter(post.views)} views + +

+ ) : null} +
+
+
+
+ ); +} + +export default BlogPostPreview; diff --git a/packages/blog-starter-kit/themes/hashnode/components/markdown-styles.module.css b/packages/blog-starter-kit/themes/hashnode/components/markdown-styles.module.css new file mode 100644 index 000000000..95d4f8b04 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/markdown-styles.module.css @@ -0,0 +1,18 @@ +.markdown { + @apply text-lg leading-relaxed; +} + +.markdown p, +.markdown ul, +.markdown ol, +.markdown blockquote { + @apply my-6; +} + +.markdown h2 { + @apply text-3xl mt-12 mb-4 leading-snug; +} + +.markdown h3 { + @apply text-2xl mt-8 mb-4 leading-snug; +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/meta.tsx b/packages/blog-starter-kit/themes/hashnode/components/meta.tsx new file mode 100644 index 000000000..578c3a741 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/meta.tsx @@ -0,0 +1,28 @@ +import parse from 'html-react-parser'; +import Head from 'next/head'; + +import { useAppContext } from './contexts/appContext'; + +export const Meta = () => { + const { publication } = useAppContext(); + const { metaTags, favicon } = publication; + const defaultFavicons = ( + <> + + + + + + + + ); + + return ( + + {favicon ? : defaultFavicons} + + + {metaTags && parse(metaTags)} + + ); +}; diff --git a/packages/blog-starter-kit/themes/hashnode/components/modern-layout-posts.tsx b/packages/blog-starter-kit/themes/hashnode/components/modern-layout-posts.tsx new file mode 100644 index 000000000..de2335d8f --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/modern-layout-posts.tsx @@ -0,0 +1,90 @@ +import { Waypoint } from 'react-waypoint'; + +import Button from './hn-button'; +import { ChevronDownSVG } from './icons/svgs'; +import { PageInfo, PublicationFragment, PostThumbnailFragment } from '../generated/graphql'; +import BlogPostPreview from './magazine-blog-post-preview'; + +const PublicationPosts = (props: { + posts: { + edges: Array<{ + cursor: string; + node: PostThumbnailFragment; + }>; + pageInfo: Pick; + }; + publication: Pick; + fetchMore: () => void; + fetching: boolean; + fetchedOnce: boolean; +}) => { + const { posts, publication, fetchMore, fetching, fetchedOnce } = props; + const { edges, pageInfo } = posts; + + const slicedPosts = edges.map((edge) => edge.node).slice(3); + + return ( +
+
+ {slicedPosts.map((post) => ( + + ))} + {fetching && ( + <> +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + )} + {pageInfo.hasNextPage && !fetchedOnce && !fetching ? ( +
+ +
+ ) : null} +
+ {fetchedOnce && pageInfo.hasNextPage ? : null} + {fetchedOnce && !pageInfo.hasNextPage ? ( +
+

You've reached the end! 👋

+
+ ) : null} +
+ ); +}; + +export default PublicationPosts; diff --git a/packages/blog-starter-kit/themes/hashnode/components/other-posts-of-account.tsx b/packages/blog-starter-kit/themes/hashnode/components/other-posts-of-account.tsx new file mode 100644 index 000000000..1308b7f0e --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/other-posts-of-account.tsx @@ -0,0 +1,110 @@ +import { twJoin } from 'tailwind-merge'; +import Link from 'next/link'; +import { resizeImage } from '../utils/image'; +import ProfileImage from './profile-image'; +import CustomImage from './custom-image'; +import { PostFullFragment } from '../generated/graphql'; + + +type Props = { + post: PostFullFragment; + morePosts: any +}; + +function OtherPostsOfAccount(props: Props) { + const { morePosts, post } = props; + + if (!morePosts || morePosts.length === 0) { + return
; + } + + const morePostsRendered = morePosts.map((postNode: any) => { + const post = postNode.node; + const postURL = `/${post.slug}`; + return ( +
+
+ {post.author && ( + + )} + {post.coverImage && ( + + + + )} +
+

+ + {post.title.substring(0, 100)} + {post.title.length > 100 ? '…' : ''} + +

+ {post.brief && ( +

+ + {post.brief.substring(0, 100)} + {post.brief.length > 100 ? '…' : ''} + +

+ )} +
+
+
+ ); + }); + + return ( +
+

+ {true ? 'More articles' : `More Stories by ${post.author.name}`} +

+
+ {morePostsRendered} +
+
+ ); +} + + + +export default OtherPostsOfAccount; diff --git a/packages/blog-starter-kit/themes/hashnode/components/post-author-info.tsx b/packages/blog-starter-kit/themes/hashnode/components/post-author-info.tsx new file mode 100644 index 000000000..0b806ea2c --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/post-author-info.tsx @@ -0,0 +1,71 @@ +import { twJoin } from 'tailwind-merge'; + +import CustomImage from './custom-image'; +import { getBlurHash, resizeImage } from '../utils/image'; + +function PostAuthorInfo(props: any) { + const { + author, + } = props; + + return ( +
+
+
+ + + +
+
+
+

+ {author.name} +

+
+ {author.bio?.html && ( +
+
+
+ )} +
+
+ {author.bio?.html && ( +
+
+
+ )} +
+ ); +} + +export default PostAuthorInfo; diff --git a/packages/blog-starter-kit/themes/hashnode/components/post-comments-sidebar.tsx b/packages/blog-starter-kit/themes/hashnode/components/post-comments-sidebar.tsx new file mode 100644 index 000000000..79bfd02b8 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/post-comments-sidebar.tsx @@ -0,0 +1,31 @@ +import { PostFullFragment } from '../generated/graphql'; +import CommentsSheet from './comments-sheet'; +import ResponseList from './response-list'; + +const PostCommentsSidebar = ({ + hideSidebar, + isPublicationPost, + selectedFilter, + post, +}: { + hideSidebar: () => void; + isPublicationPost: boolean; + selectedFilter: string; + post: PostFullFragment; +}) => ( + + + {!post.preferences.disableComments ? ( + + ) : ( +
+

The comments have been disabled by the author for this article

+
+ )} +
+); + +export default PostCommentsSidebar; diff --git a/packages/blog-starter-kit/themes/hashnode/components/post-comments.tsx b/packages/blog-starter-kit/themes/hashnode/components/post-comments.tsx new file mode 100644 index 000000000..87b61eb96 --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/post-comments.tsx @@ -0,0 +1,171 @@ +import moment from 'dayjs'; + +import localizedFormat from 'dayjs/plugin/localizedFormat'; +import relativeTime from 'dayjs/plugin/relativeTime'; + +import { twJoin } from 'tailwind-merge'; +import { formatDate } from '../utils'; +import Autolinker from '../utils/autolinker'; +import { imageReplacer } from '../utils/image'; +import { useAppContext } from './contexts/appContext'; +import { Button } from './custom-button'; +import { ExternalArrowSVG, HashnodeSVG } from './icons'; +import ProfileImage from './profile-image'; +import ResponseFooter from './response-footer'; + +moment.extend(relativeTime); +moment.extend(localizedFormat); + +export const PostComments = () => { + const { post } = useAppContext(); + if (!post) return null; + const discussionUrl = `https://hashnode.com/discussions/post/${post.id}`; + const checkIfCommentByAuthor = (comment: any) => { + return comment.author.id.toString() === post.author.id.toString(); + }; + + const loadProfile = (e: any, comment: any) => { + e.preventDefault(); + const isPublication = true; + const url = `${isPublication ? 'https://hashnode.com/@' : '/@'}${comment.author.username}`; + if (isPublication) { + window.location.href = url; + } + return null; + // Router.push(url); + }; + + const noop = (e: any) => { + e.preventDefault(); + }; + + const commentsList = post.comments.edges.map((edge) => { + const comment = edge.node as any; + return ( +
+
+ +
+
string; getTwitterHandle: () => void }, + ) { + switch (match.getType()) { + case 'twitter': + // eslint-disable-next-line no-case-declarations + const username = match.getTwitterHandle(); + return `@${username}`; + default: + return null; + } + }, + }), + ), + }} + /> + +
+ ); + }); + + return ( +
+
+
+

+ Comments{' '} + {post.responseCount > 0 ? ( + ({(post.responseCount || 0) + (post.replyCount || 0)}) + ) : ( + '' + )} +

+
+
+
{commentsList}
+
+
+
+ ); +}; diff --git a/packages/blog-starter-kit/themes/hashnode/components/post-floating-bar-tooltip-wrapper.tsx b/packages/blog-starter-kit/themes/hashnode/components/post-floating-bar-tooltip-wrapper.tsx new file mode 100644 index 000000000..bddbedf4b --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/post-floating-bar-tooltip-wrapper.tsx @@ -0,0 +1,36 @@ +import * as Tooltip from '@radix-ui/react-tooltip'; +import React from 'react'; +import { twMerge } from 'tailwind-merge'; + +export default function PostFloatingBarTooltipWrapper({ + children, + label, + asChild = true, + labelSide = 'top', + contentClassName = '', + delayDuration, +}: { + children: React.ReactChild; + label: string; + asChild?: boolean; + labelSide?: 'top' | 'right' | 'bottom' | 'left'; + contentClassName?: string; + delayDuration?: number; +}) { + return ( + + + {children} + + + + {label} + + + + ); +} diff --git a/packages/blog-starter-kit/themes/hashnode/components/post-floating-bar.tsx b/packages/blog-starter-kit/themes/hashnode/components/post-floating-bar.tsx new file mode 100644 index 000000000..e5d41d1bc --- /dev/null +++ b/packages/blog-starter-kit/themes/hashnode/components/post-floating-bar.tsx @@ -0,0 +1,144 @@ +import * as Tooltip from '@radix-ui/react-tooltip'; +import { useEffect } from 'react'; + +import { CommentSVGV2 } from './icons/svgs'; +import { kFormatter } from '../utils/image'; +import { Separator } from './separator-root'; +import PostFloatingBarTooltipWrapper from './post-floating-bar-tooltip-wrapper'; +import { PostFullFragment } from '../generated/graphql'; +import TocSheet from './toc-sheet'; +import PostShareWidget from './post-share-widget'; + + +function PostFloatingMenu(props: { + isPublicationPost: boolean; + post: PostFullFragment; + shareText: string; + showPaymentModal?: () => void; + openComments?: () => void; + list: any[]; +}) { + const { + isPublicationPost, + post, + shareText, + showPaymentModal, + openComments, + list, + } = props; + + const handleFloatingBarDisplay = () => { + const blogHeader = document.querySelector('.blog-header'); + const blogContent = document.querySelector('#post-content-parent'); + const floatingBar = document.querySelector('.post-floating-bar'); + + if (!floatingBar?.classList.contains('freeze')) { + if (window.scrollY > blogHeader!.clientHeight) { + floatingBar?.classList.add('active', 'animation'); + } else if (floatingBar?.classList.contains('active')) { + floatingBar?.classList.remove('active'); + } + } + + const currentViewportHeight = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0); + // Adding 40 as a buffer to adjust the trigger + const isPostContentBottomInsideViewport = blogContent!.getBoundingClientRect().bottom + 40 <= currentViewportHeight; + // Adding 175 as a buffer to adjust the trigger + const isPostContentBottomAlmostOut = + window.scrollY - currentViewportHeight - 175 <= blogContent!.clientHeight && + floatingBar?.classList.contains('freeze'); + + if (isPostContentBottomInsideViewport) { + floatingBar?.classList.remove('active'); + floatingBar?.classList.add('freeze'); + } else if (isPostContentBottomAlmostOut) { + floatingBar?.classList.remove('freeze', 'animation'); + floatingBar?.classList.add('active'); + } + }; + + useEffect(() => { + handleFloatingBarDisplay(); + window.addEventListener('scroll', handleFloatingBarDisplay); + return () => { + window.removeEventListener('scroll', handleFloatingBarDisplay); + }; + }, []); + + // Best practice to have the accessible name being with the visible text (comment count) + const commentBtnAccessibleLabel = + post?.responseCount > 0 + ? `${kFormatter(post.responseCount + (post.replyCount || 0))} comment${ + post.responseCount === 1 ? '' : 's' + }, open the comments` + : 'Open comments'; + + return ( + + + + + + ); +}; + +const Page = ({ page }: PageProps) => { + const title = page.title; + return ( + <> + + {title} + +
+ +
+ + ); +}; + +export default function PostOrPage(props: Props) { + const headerRef = useRef(null); + const maybePost = props.type === 'post' ? props.post : null; + const maybePage = props.type === 'page' ? props.page : null; + const publication = props.publication; + const navPositionStyles = + 'relative transform-none md:sticky md:top-0 md:left-0 md:backdrop-blur-lg'; + + if (props.type === 'post') { + return ( + + +
+ +
+ +
+ +
+
+ +
+
+ ); + } + + const description = + publication.descriptionSEO || publication.title || `${publication.author.name}'s Blog`; + + return ( + + + + + {publication.displayTitle || publication.title || 'Hashnode Blog Starter Kit'} + + + + + + + + )} + {gTagManagerID && ( + + )} + {koalaForUsers && ( + + )} + {msClarityForUsers && ( + + )} {plausibleAnalyticsEnabled && (