Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timeline #86

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 21 additions & 1 deletion packages/blog-starter-kit/themes/enterprise/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,12 @@ export type Draft = Node & {
slug: Scalars['String']['output'];
/** The subtitle of the draft. It would become the subtitle of the post when published. */
subtitle?: Maybe<Scalars['String']['output']>;
/** Returns list of tags added to the draft. Contains tag id, name, slug, etc. */
/**
* Returns list of tags added to the draft. Contains tag id, name, slug, etc.
* @deprecated Use tagsV2 instead. Will be removed on 26/02/2024.
*/
tags: Array<Tag>;
tagsV2: Array<DraftTag>;
/** The title of the draft. It would become the title of the post when published. */
title?: Maybe<Scalars['String']['output']>;
updatedAt: Scalars['DateTime']['output'];
Expand All @@ -383,6 +387,18 @@ export type DraftBackup = {
status?: Maybe<BackupStatus>;
};

/**
* Contains basic information about a Tag within a Draft.
* A tag in a draft is a tag that is not published yet.
*/
export type DraftBaseTag = {
__typename?: 'DraftBaseTag';
/** The name of the tag. Shown in tag page. */
name: Scalars['String']['output'];
/** The slug of the tag. Used to access tags feed. Example https://hashnode.com/n/graphql */
slug: Scalars['String']['output'];
};

/**
* Connection to get list of drafts.
* Returns a list of edges which contains the draft and cursor to the last item of the previous page.
Expand Down Expand Up @@ -434,6 +450,8 @@ export type DraftSettings = {
stickCoverToBottom: Scalars['Boolean']['output'];
};

export type DraftTag = DraftBaseTag | Tag;

/**
* An edge that contains a node and cursor to the node.
* This is a common interface for all edges.
Expand Down Expand Up @@ -2145,6 +2163,7 @@ export enum Scope {
CreatePro = 'create_pro',
ImportSubscribersToPublication = 'import_subscribers_to_publication',
PublicationAdmin = 'publication_admin',
PublicationMember = 'publication_member',
PublishComment = 'publish_comment',
PublishDraft = 'publish_draft',
PublishPost = 'publish_post',
Expand All @@ -2158,6 +2177,7 @@ export enum Scope {
UpdatePost = 'update_post',
UpdateReply = 'update_reply',
WebhookAdmin = 'webhook_admin',
WriteDraft = 'write_draft',
WritePost = 'write_post',
WriteSeries = 'write_series'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,8 @@ type Draft implements Node {
"""
Returns list of tags added to the draft. Contains tag id, name, slug, etc.
"""
tags: [Tag!]!
tags: [Tag!]! @deprecated(reason: "Use tagsV2 instead. Will be removed on 26/02/2024.")
tagsV2: [DraftTag!]!
"""
The title of the draft. It would become the title of the post when published.
"""
Expand All @@ -384,6 +385,19 @@ type DraftBackup {
status: BackupStatus
}

"""
Contains basic information about a Tag within a Draft.
A tag in a draft is a tag that is not published yet.
"""
type DraftBaseTag {
"""The name of the tag. Shown in tag page."""
name: String!
"""
The slug of the tag. Used to access tags feed. Example https://hashnode.com/n/graphql
"""
slug: String!
}

"""
Connection to get list of drafts.
Returns a list of edges which contains the draft and cursor to the last item of the previous page.
Expand Down Expand Up @@ -432,6 +446,8 @@ type DraftSettings {
stickCoverToBottom: Boolean!
}

union DraftTag = DraftBaseTag | Tag

"""
An edge that contains a node and cursor to the node.
This is a common interface for all edges.
Expand Down Expand Up @@ -2025,6 +2041,7 @@ enum Scope {
create_pro
import_subscribers_to_publication
publication_admin
publication_member
publish_comment
publish_draft
publish_post
Expand All @@ -2038,6 +2055,7 @@ enum Scope {
update_post
update_reply
webhook_admin
write_draft
write_post
write_series
}
Expand Down
3 changes: 3 additions & 0 deletions packages/blog-starter-kit/themes/timeline/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT=
NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST=
NEXT_PUBLIC_MODE=
4 changes: 4 additions & 0 deletions packages/blog-starter-kit/themes/timeline/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['@starter-kit/eslint-config-custom'],
};
2 changes: 2 additions & 0 deletions packages/blog-starter-kit/themes/timeline/.graphqlrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: './generated/schema.graphql'
documents: './{pages,components}/**/*.{graphql,js,ts,jsx,tsx}'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module 'remark-html';
21 changes: 21 additions & 0 deletions packages/blog-starter-kit/themes/timeline/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Shreyas Chaliha aka Trace

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.
31 changes: 31 additions & 0 deletions packages/blog-starter-kit/themes/timeline/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 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).

We've used [Hashnode API's](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.

## 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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
23 changes: 23 additions & 0 deletions packages/blog-starter-kit/themes/timeline/codegen.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
schema: https://gql.hashnode.com
documents: './**/*.graphql'
generates:
./generated/schema.graphql:
plugins:
- schema-ast
config:
includeDirectives: true
./generated/graphql.ts:
plugins:
- typescript
- typescript-operations
- typed-document-node
config:
scalars:
Date: string
DateTime: string
ObjectId: string
JSONObject: Record<string, unknown>
Decimal: string
CurrencyCode: string
ImageContentType: string
ImageUrl: string
16 changes: 16 additions & 0 deletions packages/blog-starter-kit/themes/timeline/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "styles/index.css",
"baseColor": "zinc",
"cssVariables": true
},
"aliases": {
"components": "components",
"utils": "lib/utils"
}
}
142 changes: 142 additions & 0 deletions packages/blog-starter-kit/themes/timeline/components/analytics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import Cookies from 'js-cookie';
import { useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
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 _sendPageViewsToHashnodeGoogleAnalytics = () => {
// @ts-ignore
window.gtag('config', GA_TRACKING_ID, {
transport_url: 'https://ping.hashnode.com',
first_party_collection: true,
});
};

const _sendViewsToHashnodeInternalAnalytics = async () => {
// Send to Hashnode's own internal analytics
const event: Record<string, string | number | object> = {
event_type: 'pageview',
time: new Date().getTime(),
event_properties: {
hostname: window.location.hostname,
url: window.location.pathname,
eventType: 'pageview',
publicationId: publication.id,
dateAdded: new Date().getTime(),
referrer: window.document.referrer,
},
};

let deviceId = Cookies.get('__amplitudeDeviceID');
if (!deviceId) {
deviceId = uuidv4();
Cookies.set('__amplitudeDeviceID', deviceId, {
expires: 365 * 2,
}); // expire after two years
}

event['device_id'] = deviceId;

await fetch(`${BASE_PATH}/ping/data-event`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ events: [event] }),
});
};

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 = '';
}

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,
};

// 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',
}),
});
}

// For Hashnode Blog Dashboard Analytics
fetch(`${BASE_PATH}/ping/view`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ data }),
});
};

return null;
};