mirror of
https://github.com/AderKonstantin/aderktech-chronark.com-.git
synced 2025-06-08 13:48:42 +03:00
feat: analytics
This commit is contained in:
parent
d13531c222
commit
88e2b1dc16
@ -7,12 +7,12 @@ import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||
var computedFields = {
|
||||
path: {
|
||||
type: "string",
|
||||
resolve: (doc) => `/${doc._raw.flattenedPath}`,
|
||||
resolve: (doc) => `/${doc._raw.flattenedPath}`
|
||||
},
|
||||
slug: {
|
||||
type: "string",
|
||||
resolve: (doc) => doc._raw.flattenedPath.split("/").slice(1).join("/"),
|
||||
},
|
||||
resolve: (doc) => doc._raw.flattenedPath.split("/").slice(1).join("/")
|
||||
}
|
||||
};
|
||||
var Project = defineDocumentType(() => ({
|
||||
name: "Project",
|
||||
@ -20,27 +20,27 @@ var Project = defineDocumentType(() => ({
|
||||
contentType: "mdx",
|
||||
fields: {
|
||||
published: {
|
||||
type: "boolean",
|
||||
type: "boolean"
|
||||
},
|
||||
title: {
|
||||
type: "string",
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
date: {
|
||||
type: "date",
|
||||
type: "date"
|
||||
},
|
||||
url: {
|
||||
type: "string",
|
||||
type: "string"
|
||||
},
|
||||
repository: {
|
||||
type: "string",
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
},
|
||||
computedFields,
|
||||
computedFields
|
||||
}));
|
||||
var Page = defineDocumentType(() => ({
|
||||
name: "Page",
|
||||
@ -49,13 +49,13 @@ var Page = defineDocumentType(() => ({
|
||||
fields: {
|
||||
title: {
|
||||
type: "string",
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
},
|
||||
computedFields,
|
||||
computedFields
|
||||
}));
|
||||
var contentlayer_config_default = makeSource({
|
||||
contentDirPath: "./content",
|
||||
@ -78,20 +78,24 @@ var contentlayer_config_default = makeSource({
|
||||
},
|
||||
onVisitHighlightedWord(node) {
|
||||
node.properties.className = ["word--highlighted"];
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
rehypeAutolinkHeadings,
|
||||
{
|
||||
properties: {
|
||||
className: ["subheading-anchor"],
|
||||
ariaLabel: "Link to section",
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
ariaLabel: "Link to section"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
});
|
||||
export { Page, Project, contentlayer_config_default as default };
|
||||
export {
|
||||
Page,
|
||||
Project,
|
||||
contentlayer_config_default as default
|
||||
};
|
||||
//# sourceMappingURL=compiled-contentlayer-config-AAEZAM7W.mjs.map
|
||||
|
@ -1,3 +1,5 @@
|
||||
// NOTE This file is auto-generated by Contentlayer
|
||||
|
||||
export const allPages = [];
|
||||
|
||||
|
||||
export const allPages = []
|
||||
|
@ -1,65 +1,19 @@
|
||||
// NOTE This file is auto-generated by Contentlayer
|
||||
|
||||
import projects__accessMdx from "./projects__access.mdx.json" assert {
|
||||
type: "json",
|
||||
};
|
||||
import projects__envshareMdx from "./projects__envshare.mdx.json" assert {
|
||||
type: "json",
|
||||
};
|
||||
import projects__planetfallMdx from "./projects__planetfall.mdx.json" assert {
|
||||
type: "json",
|
||||
};
|
||||
import projects__qstashMdx from "./projects__qstash.mdx.json" assert {
|
||||
type: "json",
|
||||
};
|
||||
import projects__terraformProviderVercelMdx from "./projects__terraform-provider-vercel.mdx.json" assert {
|
||||
type: "json",
|
||||
};
|
||||
import projects__upstashAuthAnalyticsMdx from "./projects__upstash-auth-analytics.mdx.json" assert {
|
||||
type: "json",
|
||||
};
|
||||
import projects__upstashCliMdx from "./projects__upstash-cli.mdx.json" assert {
|
||||
type: "json",
|
||||
};
|
||||
import projects__upstashCoreAnalyticsMdx from "./projects__upstash-core-analytics.mdx.json" assert {
|
||||
type: "json",
|
||||
};
|
||||
import projects__upstashEdgeFlagsMdx from "./projects__upstash-edge-flags.mdx.json" assert {
|
||||
type: "json",
|
||||
};
|
||||
import projects__upstashKafkaMdx from "./projects__upstash-kafka.mdx.json" assert {
|
||||
type: "json",
|
||||
};
|
||||
import projects__upstashQstashSdkMdx from "./projects__upstash-qstash-sdk.mdx.json" assert {
|
||||
type: "json",
|
||||
};
|
||||
import projects__upstashRatelimitMdx from "./projects__upstash-ratelimit.mdx.json" assert {
|
||||
type: "json",
|
||||
};
|
||||
import projects__upstashReactUiMdx from "./projects__upstash-react-ui.mdx.json" assert {
|
||||
type: "json",
|
||||
};
|
||||
import projects__upstashRedisMdx from "./projects__upstash-redis.mdx.json" assert {
|
||||
type: "json",
|
||||
};
|
||||
import projects__upstashWebAnalyticsMdx from "./projects__upstash-web-analytics.mdx.json" assert {
|
||||
type: "json",
|
||||
};
|
||||
import projects__accessMdx from './projects__access.mdx.json' assert { type: 'json' }
|
||||
import projects__envshareMdx from './projects__envshare.mdx.json' assert { type: 'json' }
|
||||
import projects__planetfallMdx from './projects__planetfall.mdx.json' assert { type: 'json' }
|
||||
import projects__qstashMdx from './projects__qstash.mdx.json' assert { type: 'json' }
|
||||
import projects__terraformProviderVercelMdx from './projects__terraform-provider-vercel.mdx.json' assert { type: 'json' }
|
||||
import projects__upstashAuthAnalyticsMdx from './projects__upstash-auth-analytics.mdx.json' assert { type: 'json' }
|
||||
import projects__upstashCliMdx from './projects__upstash-cli.mdx.json' assert { type: 'json' }
|
||||
import projects__upstashCoreAnalyticsMdx from './projects__upstash-core-analytics.mdx.json' assert { type: 'json' }
|
||||
import projects__upstashEdgeFlagsMdx from './projects__upstash-edge-flags.mdx.json' assert { type: 'json' }
|
||||
import projects__upstashKafkaMdx from './projects__upstash-kafka.mdx.json' assert { type: 'json' }
|
||||
import projects__upstashQstashSdkMdx from './projects__upstash-qstash-sdk.mdx.json' assert { type: 'json' }
|
||||
import projects__upstashRatelimitMdx from './projects__upstash-ratelimit.mdx.json' assert { type: 'json' }
|
||||
import projects__upstashReactUiMdx from './projects__upstash-react-ui.mdx.json' assert { type: 'json' }
|
||||
import projects__upstashRedisMdx from './projects__upstash-redis.mdx.json' assert { type: 'json' }
|
||||
import projects__upstashWebAnalyticsMdx from './projects__upstash-web-analytics.mdx.json' assert { type: 'json' }
|
||||
|
||||
export const allProjects = [
|
||||
projects__accessMdx,
|
||||
projects__envshareMdx,
|
||||
projects__planetfallMdx,
|
||||
projects__qstashMdx,
|
||||
projects__terraformProviderVercelMdx,
|
||||
projects__upstashAuthAnalyticsMdx,
|
||||
projects__upstashCliMdx,
|
||||
projects__upstashCoreAnalyticsMdx,
|
||||
projects__upstashEdgeFlagsMdx,
|
||||
projects__upstashKafkaMdx,
|
||||
projects__upstashQstashSdkMdx,
|
||||
projects__upstashRatelimitMdx,
|
||||
projects__upstashReactUiMdx,
|
||||
projects__upstashRedisMdx,
|
||||
projects__upstashWebAnalyticsMdx,
|
||||
];
|
||||
export const allProjects = [projects__accessMdx, projects__envshareMdx, projects__planetfallMdx, projects__qstashMdx, projects__terraformProviderVercelMdx, projects__upstashAuthAnalyticsMdx, projects__upstashCliMdx, projects__upstashCoreAnalyticsMdx, projects__upstashEdgeFlagsMdx, projects__upstashKafkaMdx, projects__upstashQstashSdkMdx, projects__upstashRatelimitMdx, projects__upstashReactUiMdx, projects__upstashRedisMdx, projects__upstashWebAnalyticsMdx]
|
||||
|
11
.contentlayer/generated/index.d.ts
vendored
11
.contentlayer/generated/index.d.ts
vendored
@ -1,10 +1,11 @@
|
||||
// NOTE This file is auto-generated by Contentlayer
|
||||
|
||||
import { Page, Project, DocumentTypes } from "./types";
|
||||
import { Page, Project, DocumentTypes } from './types'
|
||||
|
||||
export * from "./types";
|
||||
export * from './types'
|
||||
|
||||
export declare const allPages: Page[];
|
||||
export declare const allProjects: Project[];
|
||||
export declare const allPages: Page[]
|
||||
export declare const allProjects: Project[]
|
||||
|
||||
export declare const allDocuments: DocumentTypes[]
|
||||
|
||||
export declare const allDocuments: DocumentTypes[];
|
||||
|
@ -1,12 +1,12 @@
|
||||
// NOTE This file is auto-generated by Contentlayer
|
||||
|
||||
export { isType } from "contentlayer/client";
|
||||
export { isType } from 'contentlayer/client'
|
||||
|
||||
// NOTE During development Contentlayer imports from `.mjs` files to improve HMR speeds.
|
||||
// During (production) builds Contentlayer it imports from `.json` files to improve build performance.
|
||||
import allPages from "./Page/_index.json" assert { type: "json" };
|
||||
import allProjects from "./Project/_index.json" assert { type: "json" };
|
||||
import { allPages } from './Page/_index.mjs'
|
||||
import { allProjects } from './Project/_index.mjs'
|
||||
|
||||
export { allPages, allProjects };
|
||||
export { allPages, allProjects }
|
||||
|
||||
export const allDocuments = [...allPages, ...allProjects];
|
||||
export const allDocuments = [...allPages, ...allProjects]
|
||||
|
97
.contentlayer/generated/types.d.ts
vendored
97
.contentlayer/generated/types.d.ts
vendored
@ -1,69 +1,66 @@
|
||||
// NOTE This file is auto-generated by Contentlayer
|
||||
|
||||
import type {
|
||||
Markdown,
|
||||
MDX,
|
||||
ImageFieldData,
|
||||
IsoDateTimeString,
|
||||
} from "contentlayer/core";
|
||||
import * as Local from "contentlayer/source-files";
|
||||
import type { Markdown, MDX, ImageFieldData, IsoDateTimeString } from 'contentlayer/core'
|
||||
import * as Local from 'contentlayer/source-files'
|
||||
|
||||
export { isType } from "contentlayer/client";
|
||||
export { isType } from 'contentlayer/client'
|
||||
|
||||
export type { Markdown, MDX, ImageFieldData, IsoDateTimeString };
|
||||
export type { Markdown, MDX, ImageFieldData, IsoDateTimeString }
|
||||
|
||||
/** Document types */
|
||||
export type Page = {
|
||||
/** File path relative to `contentDirPath` */
|
||||
_id: string;
|
||||
_raw: Local.RawDocumentData;
|
||||
type: "Page";
|
||||
title: string;
|
||||
description?: string | undefined;
|
||||
_id: string
|
||||
_raw: Local.RawDocumentData
|
||||
type: 'Page'
|
||||
title: string
|
||||
description?: string | undefined
|
||||
/** MDX file body */
|
||||
body: MDX;
|
||||
path: string;
|
||||
slug: string;
|
||||
};
|
||||
body: MDX
|
||||
path: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
export type Project = {
|
||||
/** File path relative to `contentDirPath` */
|
||||
_id: string;
|
||||
_raw: Local.RawDocumentData;
|
||||
type: "Project";
|
||||
published?: boolean | undefined;
|
||||
title: string;
|
||||
description: string;
|
||||
date?: IsoDateTimeString | undefined;
|
||||
url?: string | undefined;
|
||||
repository?: string | undefined;
|
||||
_id: string
|
||||
_raw: Local.RawDocumentData
|
||||
type: 'Project'
|
||||
published?: boolean | undefined
|
||||
title: string
|
||||
description: string
|
||||
date?: IsoDateTimeString | undefined
|
||||
url?: string | undefined
|
||||
repository?: string | undefined
|
||||
/** MDX file body */
|
||||
body: MDX;
|
||||
path: string;
|
||||
slug: string;
|
||||
};
|
||||
body: MDX
|
||||
path: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
/** Nested types */
|
||||
|
||||
|
||||
/** Helper types */
|
||||
|
||||
export type AllTypes = DocumentTypes | NestedTypes;
|
||||
export type AllTypeNames = DocumentTypeNames | NestedTypeNames;
|
||||
export type AllTypes = DocumentTypes | NestedTypes
|
||||
export type AllTypeNames = DocumentTypeNames | NestedTypeNames
|
||||
|
||||
export type DocumentTypes = Page | Project;
|
||||
export type DocumentTypeNames = "Page" | "Project";
|
||||
export type DocumentTypes = Page | Project
|
||||
export type DocumentTypeNames = 'Page' | 'Project'
|
||||
|
||||
export type NestedTypes = never
|
||||
export type NestedTypeNames = never
|
||||
|
||||
export type NestedTypes = never;
|
||||
export type NestedTypeNames = never;
|
||||
|
||||
export interface ContentlayerGenTypes {
|
||||
documentTypes: DocumentTypes;
|
||||
documentTypeMap: DocumentTypeMap;
|
||||
documentTypeNames: DocumentTypeNames;
|
||||
nestedTypes: NestedTypes;
|
||||
nestedTypeMap: NestedTypeMap;
|
||||
nestedTypeNames: NestedTypeNames;
|
||||
allTypeNames: AllTypeNames;
|
||||
documentTypes: DocumentTypes
|
||||
documentTypeMap: DocumentTypeMap
|
||||
documentTypeNames: DocumentTypeNames
|
||||
nestedTypes: NestedTypes
|
||||
nestedTypeMap: NestedTypeMap
|
||||
nestedTypeNames: NestedTypeNames
|
||||
allTypeNames: AllTypeNames
|
||||
}
|
||||
|
||||
declare global {
|
||||
@ -71,8 +68,12 @@ declare global {
|
||||
}
|
||||
|
||||
export type DocumentTypeMap = {
|
||||
Page: Page;
|
||||
Project: Project;
|
||||
};
|
||||
Page: Page
|
||||
Project: Project
|
||||
}
|
||||
|
||||
export type NestedTypeMap = {
|
||||
|
||||
}
|
||||
|
||||
|
||||
export type NestedTypeMap = {};
|
||||
|
@ -9,8 +9,8 @@ import {
|
||||
import { MouseEventHandler, PropsWithChildren } from "react";
|
||||
|
||||
export const Card: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const mouseX = useSpring(0, { stiffness: 200, damping: 100 });
|
||||
const mouseY = useSpring(0, { stiffness: 200, damping: 100 });
|
||||
const mouseX = useSpring(0, { stiffness: 500, damping: 100 });
|
||||
const mouseY = useSpring(0, { stiffness: 500, damping: 100 });
|
||||
|
||||
function onMouseMove({ currentTarget, clientX, clientY }: any) {
|
||||
const { left, top } = currentTarget.getBoundingClientRect();
|
||||
@ -23,7 +23,7 @@ export const Card: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
return (
|
||||
<div
|
||||
onMouseMove={onMouseMove}
|
||||
className="overflow-hidden relative duration-700 border rounded-xl hover:bg-zinc-800/30 group md:gap-8 hover:border-zinc-400/50 border-zinc-600 "
|
||||
className="overflow-hidden relative duration-700 border rounded-xl hover:bg-zinc-800/10 group md:gap-8 hover:border-zinc-400/50 border-zinc-600 "
|
||||
>
|
||||
<div className="pointer-events-none">
|
||||
<div className="absolute inset-0 z-0 transition duration-1000 [mask-image:linear-gradient(black,transparent)]" />
|
||||
|
BIN
app/favicon.ico
BIN
app/favicon.ico
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
@ -7,10 +7,11 @@ export const metadata: Metadata = {
|
||||
default: "chronark.com",
|
||||
template: "%s | chronark.com",
|
||||
},
|
||||
description: "software engineer at Upstash and founder of planetfall.io",
|
||||
description: "Software engineer at upstash.com and founder of planetfall.io",
|
||||
openGraph: {
|
||||
title: "chronark.com",
|
||||
description: "software engineer at Upstash and founder of planetfall.io",
|
||||
description:
|
||||
"Software engineer at upstash.com and founder of planetfall.io",
|
||||
url: "https://chronark.com",
|
||||
siteName: "chronark.com",
|
||||
images: [
|
||||
|
@ -3,6 +3,7 @@ import { allProjects } from "contentlayer/generated";
|
||||
import { Mdx } from "@/app/components/mdx";
|
||||
import { Header } from "./header";
|
||||
import "./mdx.css";
|
||||
import { ReportView } from "./view";
|
||||
|
||||
type Props = {
|
||||
params: {
|
||||
@ -29,6 +30,7 @@ export default async function PostPage({ params }: Props) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Header project={project} />
|
||||
<ReportView slug={project.slug} />
|
||||
|
||||
<main className="bg-zinc-50">
|
||||
<article className="px-4 py-12 mx-auto prose prose-zinc">
|
||||
|
17
app/projects/[slug]/view.tsx
Normal file
17
app/projects/[slug]/view.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const ReportView: React.FC<{ slug: string }> = ({ slug }) => {
|
||||
useEffect(() => {
|
||||
fetch("/api/incr", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ slug }),
|
||||
});
|
||||
}, [slug]);
|
||||
|
||||
return null;
|
||||
};
|
@ -75,7 +75,7 @@ export default function ProjectsPage() {
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<div className="flex flex-col w-full max-w-2xl gap-8 pt-12 mx-auto border-t border-gray-900/10 sm:pt-16 lg:mx-0 lg:max-w-none lg:border-t-0 lg:pt-0">
|
||||
<div className="flex flex-col w-full gap-8 mx-auto border-t border-gray-900/10 lg:pt-16 lg:mx-0 lg:border-t-0 ">
|
||||
{[top2, top3].map((project) => (
|
||||
<Card key={project.slug}>
|
||||
<Article project={project} />
|
||||
@ -85,7 +85,7 @@ export default function ProjectsPage() {
|
||||
</div>
|
||||
<div className="hidden w-full h-px md:block bg-zinc-800" />
|
||||
|
||||
<div className="grid max-w-2xl grid-cols-1 gap-4 mx-auto lg:mx-0 lg:max-w-none md:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="grid grid-cols-1 gap-4 mx-auto lg:mx-0 md:grid-cols-3">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{sorted
|
||||
.filter((_, i) => i % 3 === 0)
|
||||
|
@ -1,41 +0,0 @@
|
||||
import { Redis } from "@upstash/redis";
|
||||
import { NextRequest, NextResponse, NextFetchEvent } from "next/server";
|
||||
|
||||
const redis = Redis.fromEnv();
|
||||
export const config = {
|
||||
runtime: "experimental-edge",
|
||||
matcher: "/projects/:slug*",
|
||||
};
|
||||
|
||||
export default async function middleware(
|
||||
req: NextRequest,
|
||||
evt: NextFetchEvent,
|
||||
): Promise<NextResponse> {
|
||||
const path = new URL(req.url).pathname;
|
||||
console.log({ path });
|
||||
|
||||
evt.waitUntil(incrementPageView(req.ip, path));
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
async function incrementPageView(
|
||||
identifier: string | undefined,
|
||||
pathname: string,
|
||||
): Promise<void> {
|
||||
if (identifier) {
|
||||
// deduplicate the ip for each slug
|
||||
const isNew = await redis.set(
|
||||
["deduplicate", identifier, pathname].join(":"),
|
||||
true,
|
||||
{
|
||||
nx: true,
|
||||
ex: 24 * 60 * 60,
|
||||
},
|
||||
);
|
||||
if (!isNew) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await redis.incr(["pageviews", pathname].join(":"));
|
||||
}
|
43
pages/api/incr.ts
Normal file
43
pages/api/incr.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Redis } from "@upstash/redis";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const redis = Redis.fromEnv();
|
||||
export const config = {
|
||||
runtime: "edge",
|
||||
};
|
||||
|
||||
export default async function incr(req: NextRequest): Promise<NextResponse> {
|
||||
if (req.method !== "POST") {
|
||||
return new NextResponse("use POST", { status: 405 });
|
||||
}
|
||||
if (req.headers.get("Content-Type") !== "application/json") {
|
||||
return new NextResponse("must be json", { status: 400 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
let slug: string | undefined = undefined;
|
||||
if ("slug" in body) {
|
||||
slug = body.slug;
|
||||
}
|
||||
|
||||
if (!slug) {
|
||||
return new NextResponse("Slug not found", { status: 400 });
|
||||
}
|
||||
const identifier = req.ip;
|
||||
if (identifier) {
|
||||
// deduplicate the ip for each slug
|
||||
const isNew = await redis.set(
|
||||
["deduplicate", identifier, slug].join(":"),
|
||||
true,
|
||||
{
|
||||
nx: true,
|
||||
ex: 24 * 60 * 60,
|
||||
},
|
||||
);
|
||||
if (!isNew) {
|
||||
new NextResponse(null, { status: 202 });
|
||||
}
|
||||
}
|
||||
await redis.incr(["pageviews", slug].join(":"));
|
||||
return new NextResponse(null, { status: 202 });
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user