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
c3676d2b8d
@ -5,93 +5,97 @@ import rehypePrettyCode from "rehype-pretty-code";
|
|||||||
import rehypeSlug from "rehype-slug";
|
import rehypeSlug from "rehype-slug";
|
||||||
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||||
var computedFields = {
|
var computedFields = {
|
||||||
path: {
|
path: {
|
||||||
type: "string",
|
type: "string",
|
||||||
resolve: (doc) => `/${doc._raw.flattenedPath}`,
|
resolve: (doc) => `/${doc._raw.flattenedPath}`
|
||||||
},
|
},
|
||||||
slug: {
|
slug: {
|
||||||
type: "string",
|
type: "string",
|
||||||
resolve: (doc) => doc._raw.flattenedPath.split("/").slice(1).join("/"),
|
resolve: (doc) => doc._raw.flattenedPath.split("/").slice(1).join("/")
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
var Project = defineDocumentType(() => ({
|
var Project = defineDocumentType(() => ({
|
||||||
name: "Project",
|
name: "Project",
|
||||||
filePathPattern: "./projects/**/*.mdx",
|
filePathPattern: "./projects/**/*.mdx",
|
||||||
contentType: "mdx",
|
contentType: "mdx",
|
||||||
fields: {
|
fields: {
|
||||||
published: {
|
published: {
|
||||||
type: "boolean",
|
type: "boolean"
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
type: "string",
|
type: "string",
|
||||||
required: true,
|
required: true
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: "string",
|
type: "string",
|
||||||
required: true,
|
required: true
|
||||||
},
|
},
|
||||||
date: {
|
date: {
|
||||||
type: "date",
|
type: "date"
|
||||||
},
|
},
|
||||||
url: {
|
url: {
|
||||||
type: "string",
|
type: "string"
|
||||||
},
|
},
|
||||||
repository: {
|
repository: {
|
||||||
type: "string",
|
type: "string"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
computedFields,
|
computedFields
|
||||||
}));
|
}));
|
||||||
var Page = defineDocumentType(() => ({
|
var Page = defineDocumentType(() => ({
|
||||||
name: "Page",
|
name: "Page",
|
||||||
filePathPattern: "pages/**/*.mdx",
|
filePathPattern: "pages/**/*.mdx",
|
||||||
contentType: "mdx",
|
contentType: "mdx",
|
||||||
fields: {
|
fields: {
|
||||||
title: {
|
title: {
|
||||||
type: "string",
|
type: "string",
|
||||||
required: true,
|
required: true
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: "string",
|
type: "string"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
computedFields,
|
computedFields
|
||||||
}));
|
}));
|
||||||
var contentlayer_config_default = makeSource({
|
var contentlayer_config_default = makeSource({
|
||||||
contentDirPath: "./content",
|
contentDirPath: "./content",
|
||||||
documentTypes: [Page, Project],
|
documentTypes: [Page, Project],
|
||||||
mdx: {
|
mdx: {
|
||||||
remarkPlugins: [remarkGfm],
|
remarkPlugins: [remarkGfm],
|
||||||
rehypePlugins: [
|
rehypePlugins: [
|
||||||
rehypeSlug,
|
rehypeSlug,
|
||||||
[
|
[
|
||||||
rehypePrettyCode,
|
rehypePrettyCode,
|
||||||
{
|
{
|
||||||
theme: "github-dark",
|
theme: "github-dark",
|
||||||
onVisitLine(node) {
|
onVisitLine(node) {
|
||||||
if (node.children.length === 0) {
|
if (node.children.length === 0) {
|
||||||
node.children = [{ type: "text", value: " " }];
|
node.children = [{ type: "text", value: " " }];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onVisitHighlightedLine(node) {
|
onVisitHighlightedLine(node) {
|
||||||
node.properties.className.push("line--highlighted");
|
node.properties.className.push("line--highlighted");
|
||||||
},
|
},
|
||||||
onVisitHighlightedWord(node) {
|
onVisitHighlightedWord(node) {
|
||||||
node.properties.className = ["word--highlighted"];
|
node.properties.className = ["word--highlighted"];
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
rehypeAutolinkHeadings,
|
rehypeAutolinkHeadings,
|
||||||
{
|
{
|
||||||
properties: {
|
properties: {
|
||||||
className: ["subheading-anchor"],
|
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
|
//# sourceMappingURL=compiled-contentlayer-config-AAEZAM7W.mjs.map
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
// NOTE This file is auto-generated by Contentlayer
|
// 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
|
// NOTE This file is auto-generated by Contentlayer
|
||||||
|
|
||||||
import projects__accessMdx from "./projects__access.mdx.json" assert {
|
import projects__accessMdx from './projects__access.mdx.json' assert { type: 'json' }
|
||||||
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__envshareMdx from "./projects__envshare.mdx.json" assert {
|
import projects__qstashMdx from './projects__qstash.mdx.json' assert { type: 'json' }
|
||||||
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__planetfallMdx from "./projects__planetfall.mdx.json" assert {
|
import projects__upstashCliMdx from './projects__upstash-cli.mdx.json' assert { type: 'json' }
|
||||||
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__qstashMdx from "./projects__qstash.mdx.json" assert {
|
import projects__upstashKafkaMdx from './projects__upstash-kafka.mdx.json' assert { type: 'json' }
|
||||||
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__terraformProviderVercelMdx from "./projects__terraform-provider-vercel.mdx.json" assert {
|
import projects__upstashReactUiMdx from './projects__upstash-react-ui.mdx.json' assert { type: 'json' }
|
||||||
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__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 = [
|
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]
|
||||||
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
|
// 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 allPages: Page[]
|
||||||
export declare const allProjects: Project[];
|
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
|
// 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.
|
// 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.
|
// During (production) builds Contentlayer it imports from `.json` files to improve build performance.
|
||||||
import allPages from "./Page/_index.json" assert { type: "json" };
|
import { allPages } from './Page/_index.mjs'
|
||||||
import allProjects from "./Project/_index.json" assert { type: "json" };
|
import { allProjects } from './Project/_index.mjs'
|
||||||
|
|
||||||
export { allPages, allProjects };
|
export { allPages, allProjects }
|
||||||
|
|
||||||
export const allDocuments = [...allPages, ...allProjects];
|
export const allDocuments = [...allPages, ...allProjects]
|
||||||
|
107
.contentlayer/generated/types.d.ts
vendored
107
.contentlayer/generated/types.d.ts
vendored
@ -1,78 +1,79 @@
|
|||||||
// NOTE This file is auto-generated by Contentlayer
|
// NOTE This file is auto-generated by Contentlayer
|
||||||
|
|
||||||
import type {
|
import type { Markdown, MDX, ImageFieldData, IsoDateTimeString } from 'contentlayer/core'
|
||||||
Markdown,
|
import * as Local from 'contentlayer/source-files'
|
||||||
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 */
|
/** Document types */
|
||||||
export type Page = {
|
export type Page = {
|
||||||
/** File path relative to `contentDirPath` */
|
/** File path relative to `contentDirPath` */
|
||||||
_id: string;
|
_id: string
|
||||||
_raw: Local.RawDocumentData;
|
_raw: Local.RawDocumentData
|
||||||
type: "Page";
|
type: 'Page'
|
||||||
title: string;
|
title: string
|
||||||
description?: string | undefined;
|
description?: string | undefined
|
||||||
/** MDX file body */
|
/** MDX file body */
|
||||||
body: MDX;
|
body: MDX
|
||||||
path: string;
|
path: string
|
||||||
slug: string;
|
slug: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type Project = {
|
export type Project = {
|
||||||
/** File path relative to `contentDirPath` */
|
/** File path relative to `contentDirPath` */
|
||||||
_id: string;
|
_id: string
|
||||||
_raw: Local.RawDocumentData;
|
_raw: Local.RawDocumentData
|
||||||
type: "Project";
|
type: 'Project'
|
||||||
published?: boolean | undefined;
|
published?: boolean | undefined
|
||||||
title: string;
|
title: string
|
||||||
description: string;
|
description: string
|
||||||
date?: IsoDateTimeString | undefined;
|
date?: IsoDateTimeString | undefined
|
||||||
url?: string | undefined;
|
url?: string | undefined
|
||||||
repository?: string | undefined;
|
repository?: string | undefined
|
||||||
/** MDX file body */
|
/** MDX file body */
|
||||||
body: MDX;
|
body: MDX
|
||||||
path: string;
|
path: string
|
||||||
slug: string;
|
slug: string
|
||||||
};
|
}
|
||||||
|
|
||||||
/** Nested types */
|
/** Nested types */
|
||||||
|
|
||||||
|
|
||||||
/** Helper types */
|
/** Helper types */
|
||||||
|
|
||||||
export type AllTypes = DocumentTypes | NestedTypes;
|
export type AllTypes = DocumentTypes | NestedTypes
|
||||||
export type AllTypeNames = DocumentTypeNames | NestedTypeNames;
|
export type AllTypeNames = DocumentTypeNames | NestedTypeNames
|
||||||
|
|
||||||
export type DocumentTypes = Page | Project;
|
export type DocumentTypes = Page | Project
|
||||||
export type DocumentTypeNames = "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 {
|
export interface ContentlayerGenTypes {
|
||||||
documentTypes: DocumentTypes;
|
documentTypes: DocumentTypes
|
||||||
documentTypeMap: DocumentTypeMap;
|
documentTypeMap: DocumentTypeMap
|
||||||
documentTypeNames: DocumentTypeNames;
|
documentTypeNames: DocumentTypeNames
|
||||||
nestedTypes: NestedTypes;
|
nestedTypes: NestedTypes
|
||||||
nestedTypeMap: NestedTypeMap;
|
nestedTypeMap: NestedTypeMap
|
||||||
nestedTypeNames: NestedTypeNames;
|
nestedTypeNames: NestedTypeNames
|
||||||
allTypeNames: AllTypeNames;
|
allTypeNames: AllTypeNames
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface ContentlayerGen extends ContentlayerGenTypes {}
|
interface ContentlayerGen extends ContentlayerGenTypes {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DocumentTypeMap = {
|
export type DocumentTypeMap = {
|
||||||
Page: Page;
|
Page: Page
|
||||||
Project: Project;
|
Project: Project
|
||||||
};
|
}
|
||||||
|
|
||||||
export type NestedTypeMap = {};
|
export type NestedTypeMap = {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -9,8 +9,8 @@ import {
|
|||||||
import { MouseEventHandler, PropsWithChildren } from "react";
|
import { MouseEventHandler, PropsWithChildren } from "react";
|
||||||
|
|
||||||
export const Card: React.FC<PropsWithChildren> = ({ children }) => {
|
export const Card: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
const mouseX = useSpring(0, { stiffness: 200, damping: 100 });
|
const mouseX = useSpring(0, { stiffness: 500, damping: 100 });
|
||||||
const mouseY = useSpring(0, { stiffness: 200, damping: 100 });
|
const mouseY = useSpring(0, { stiffness: 500, damping: 100 });
|
||||||
|
|
||||||
function onMouseMove({ currentTarget, clientX, clientY }: any) {
|
function onMouseMove({ currentTarget, clientX, clientY }: any) {
|
||||||
const { left, top } = currentTarget.getBoundingClientRect();
|
const { left, top } = currentTarget.getBoundingClientRect();
|
||||||
@ -23,7 +23,7 @@ export const Card: React.FC<PropsWithChildren> = ({ children }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onMouseMove={onMouseMove}
|
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="pointer-events-none">
|
||||||
<div className="absolute inset-0 z-0 transition duration-1000 [mask-image:linear-gradient(black,transparent)]" />
|
<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",
|
default: "chronark.com",
|
||||||
template: "%s | 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: {
|
openGraph: {
|
||||||
title: "chronark.com",
|
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",
|
url: "https://chronark.com",
|
||||||
siteName: "chronark.com",
|
siteName: "chronark.com",
|
||||||
images: [
|
images: [
|
||||||
|
@ -3,6 +3,7 @@ import { allProjects } from "contentlayer/generated";
|
|||||||
import { Mdx } from "@/app/components/mdx";
|
import { Mdx } from "@/app/components/mdx";
|
||||||
import { Header } from "./header";
|
import { Header } from "./header";
|
||||||
import "./mdx.css";
|
import "./mdx.css";
|
||||||
|
import { ReportView } from "./view";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: {
|
params: {
|
||||||
@ -29,6 +30,7 @@ export default async function PostPage({ params }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Header project={project} />
|
<Header project={project} />
|
||||||
|
<ReportView slug={project.slug} />
|
||||||
|
|
||||||
<main className="bg-zinc-50">
|
<main className="bg-zinc-50">
|
||||||
<article className="px-4 py-12 mx-auto prose prose-zinc">
|
<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>
|
</Link>
|
||||||
</Card>
|
</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:mx-0 lg:border-t-0 ">
|
||||||
{[top2, top3].map((project) => (
|
{[top2, top3].map((project) => (
|
||||||
<Card key={project.slug}>
|
<Card key={project.slug}>
|
||||||
<Article project={project} />
|
<Article project={project} />
|
||||||
@ -85,7 +85,7 @@ export default function ProjectsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="hidden w-full h-px md:block bg-zinc-800" />
|
<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">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
{sorted
|
{sorted
|
||||||
.filter((_, i) => i % 3 === 0)
|
.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