Compare commits
60 Commits
876d0094ca
...
renovate/c
Author | SHA1 | Date | |
---|---|---|---|
|
e53257d23b | ||
5acb6a4f81 | |||
|
7b7a3cf2a0 | ||
ee25889139 | |||
7cf1b4d16e | |||
e1b7beadbd | |||
e5a797718f | |||
b23c48246d | |||
573f13568d | |||
48973560d1 | |||
d022209305 | |||
e10d447873 | |||
|
e38d6a42de | ||
f604af5d4c | |||
1a1e854d73 | |||
a7a144dbd7 | |||
22c8034560 | |||
8436da4b95 | |||
091835f01a | |||
74b20a7d7c | |||
63afa18397 | |||
071fe82685 | |||
24ed4c96a7 | |||
e404482567 | |||
938bf248cc | |||
f8cbb90ef4 | |||
990d5c93cb | |||
a92532f3bb | |||
4e0f8957df | |||
eebf233ce9 | |||
f525a8af7c | |||
d5cb54030c | |||
72e624dfd1 | |||
a0b786ae68 | |||
ffbf485774 | |||
402962e386 | |||
adf898e3ef | |||
6a2c23229e | |||
519b688f13 | |||
447da11d71 | |||
ffc386ed93 | |||
414732bf89 | |||
|
2793e1213c | ||
40fd695d03 | |||
|
9c69c76d52 | ||
5e2946d00c | |||
cf8530c750 | |||
3d44677d82 | |||
6e949539ab | |||
45d5a0e2d4 | |||
18b31bcb7b | |||
a7da175499 | |||
24d6eb52f6 | |||
0cd347714e | |||
d364ea83b5 | |||
1614889a62 | |||
7c6fd8a5ef | |||
e26a9e0267 | |||
1634fb7520 | |||
d4209bc23d |
56
.gitea/workflows/deploy-publish.yml
Normal file
56
.gitea/workflows/deploy-publish.yml
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
name: Deploy & Publish Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
paths-ignore:
|
||||||
|
- README.md
|
||||||
|
- LICENSE
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
arch: [ "ubuntu-latest" ]
|
||||||
|
git-version: [ "2.44.0" ]
|
||||||
|
runs-on: ${{ matrix.arch }}
|
||||||
|
|
||||||
|
# Steps to run
|
||||||
|
steps:
|
||||||
|
# Checkout the repo
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# Deploy to Dokku
|
||||||
|
- name: Deploy to Dokku
|
||||||
|
uses: dokku/github-action@master
|
||||||
|
with:
|
||||||
|
git_remote_url: "ssh://dokku@10.10.70.73:22/docs"
|
||||||
|
ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
# Set up Docker Buildx
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
# Login to the Docker registry
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.rainnny.club
|
||||||
|
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
# Publish the image
|
||||||
|
- name: Build Image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
git.rainnny.club/pulseapp/docs:${{ github.sha }}
|
||||||
|
git.rainnny.club/pulseapp/docs:latest
|
||||||
|
build-args: |
|
||||||
|
GIT_REV=${{ gitea.sha }}
|
@ -1,31 +0,0 @@
|
|||||||
name: Deploy
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "master" ]
|
|
||||||
paths-ignore:
|
|
||||||
- README.md
|
|
||||||
- LICENSE
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
arch: [ "ubuntu-latest" ]
|
|
||||||
git-version: [ "2.44.0" ]
|
|
||||||
runs-on: ${{ matrix.arch }}
|
|
||||||
|
|
||||||
# Steps to run
|
|
||||||
steps:
|
|
||||||
# Checkout the repo
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
# Deploy to Dokku
|
|
||||||
- name: Deploy to Dokku
|
|
||||||
uses: dokku/github-action@master
|
|
||||||
with:
|
|
||||||
git_remote_url: "ssh://dokku@10.10.70.73:22/docs"
|
|
||||||
ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }}
|
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,7 +6,6 @@ node_modules
|
|||||||
.env*.local
|
.env*.local
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
.sentryclirc
|
.sentryclirc
|
||||||
.env
|
|
||||||
sw.*
|
sw.*
|
||||||
workbox-*
|
workbox-*
|
||||||
swe-worker-*
|
swe-worker-*
|
||||||
|
11
Dockerfile
11
Dockerfile
@ -13,7 +13,7 @@ FROM base AS builder
|
|||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY --from=depends /usr/src/app/node_modules ./node_modules
|
COPY --from=depends /usr/src/app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
RUN bun run build
|
RUN bun run build
|
||||||
|
|
||||||
|
|
||||||
@ -32,14 +32,15 @@ COPY --from=builder --chown=nextjs:nextjs /usr/src/app/.next ./.next
|
|||||||
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/public ./public
|
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/public ./public
|
||||||
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/next.config.mjs ./next.config.mjs
|
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/next.config.mjs ./next.config.mjs
|
||||||
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/package.json ./package.json
|
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/package.json ./package.json
|
||||||
|
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/docs ./docs
|
||||||
|
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
# Exposting on port 80 so we can
|
# Exposting on port 80 so we can
|
||||||
# access via a reverse proxy for Dokku
|
# access via a reverse proxy for Dokku
|
||||||
ENV HOSTNAME "0.0.0.0"
|
ENV HOSTNAME="0.0.0.0"
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
ENV PORT 80
|
ENV PORT=80
|
||||||
|
|
||||||
USER nextjs
|
USER nextjs
|
||||||
CMD node server.js
|
CMD ["node", "server.js"]
|
@ -1,3 +1,2 @@
|
|||||||
# docs
|
# docs
|
||||||
|
The source coded for the Pulse App documentation site.
|
||||||
The public documentation for Pulse App.
|
|
90
config.json
Normal file
90
config.json
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"siteName": "Pulse App",
|
||||||
|
"ogApiUrl": "https://docs.pulseapp.cc/api/og?title={title}",
|
||||||
|
"metadata": {
|
||||||
|
"title": {
|
||||||
|
"default": "Pulse Docs",
|
||||||
|
"template": "%s • Pulse Docs"
|
||||||
|
},
|
||||||
|
"description": "A lightweight service monitoring solution for tracking the availability of whatever service your heart desires!",
|
||||||
|
"openGraph": {
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"url": "https://pulseapp.cc/media/logo.png",
|
||||||
|
"width": 128,
|
||||||
|
"height": 128
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"twitter": {
|
||||||
|
"card": "summary"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewport": {
|
||||||
|
"themeColor": "#A855F7"
|
||||||
|
},
|
||||||
|
"contentSource": "https://git.rainnny.club/PulseApp/docs-content.git",
|
||||||
|
"contentEditUrl": "https://git.rainnny.club/PulseApp/docs-content/src/branch/master/{slug}{ext}",
|
||||||
|
"socialLinks": [
|
||||||
|
{
|
||||||
|
"name": "GitHub",
|
||||||
|
"tooltip": "View our Github",
|
||||||
|
"logo": "./github.svg",
|
||||||
|
"href": "https://github.com/PulseAppCC",
|
||||||
|
"navbar": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Discord",
|
||||||
|
"tooltip": "Join our Discord",
|
||||||
|
"logo": "./discord.svg",
|
||||||
|
"href": "https://discord.pulseapp.cc",
|
||||||
|
"navbar": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Email",
|
||||||
|
"tooltip": "Email us",
|
||||||
|
"logo": "Mail",
|
||||||
|
"href": "mailto:support@pulseapp.cc",
|
||||||
|
"navbar": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"footer": {
|
||||||
|
"homeUrl": "https://pulseapp.cc",
|
||||||
|
"links": {
|
||||||
|
"Resources": [
|
||||||
|
{
|
||||||
|
"name": "Support",
|
||||||
|
"href": "https://support.pulseapp.cc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jobs",
|
||||||
|
"href": "https://jobs.pulseapp.cc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Developers",
|
||||||
|
"shortName": "Devs",
|
||||||
|
"href": "https://dev.pulseapp.cc",
|
||||||
|
"external": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "System Status",
|
||||||
|
"shortName": "Status",
|
||||||
|
"href": "https://status.pulseapp.cc",
|
||||||
|
"external": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Legal": [
|
||||||
|
{
|
||||||
|
"name": "Terms & Conditions",
|
||||||
|
"shortName": "Terms",
|
||||||
|
"href": "/legal/terms"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Privacy Policy",
|
||||||
|
"shortName": "Privacy",
|
||||||
|
"href": "/legal/privacy"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: git.rainnny.club/pulseapp/docs:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: docs
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- ./config.json:/usr/src/app/config.json
|
||||||
|
- ./docs:/usr/src/app/docs
|
@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
title: '🐋 Docker'
|
|
||||||
updated: '2024-10-07'
|
|
||||||
summary: 'petentium usu tota noluisse errem elaboraret auctor.'
|
|
||||||
order: 4
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🐋 Deploying on Docker
|
|
||||||
...
|
|
@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
title: '🧩 Components'
|
|
||||||
updated: '2024-10-07'
|
|
||||||
summary: 'petentium usu tota noluisse errem elaboraret auctor.'
|
|
||||||
order: 2
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🧩 Components
|
|
||||||
...
|
|
@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
title: '✔️ Supported Services'
|
|
||||||
updated: '2024-10-07'
|
|
||||||
summary: 'petentium usu tota noluisse errem elaboraret auctor.'
|
|
||||||
order: 3
|
|
||||||
---
|
|
||||||
|
|
||||||
# ✔️ Supported Services
|
|
||||||
Below is a list of all the services that are currently supported by Pulse App. Are we missing a service? Please [open an issue](https://git.rainnny.club/PulseApp/API/issues)!
|
|
@ -1,22 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: '🚀 Introduction'
|
title: 'Example'
|
||||||
updated: '2024-10-06'
|
updated: '2024-10-06'
|
||||||
summary: 'petentium usu tota noluisse errem elaboraret auctor.'
|
summary: 'petentium usu tota noluisse errem elaboraret auctor.'
|
||||||
order: 1
|
order: 1
|
||||||
---
|
---
|
||||||
|
|
||||||
> [!IMPORTANT]
|
# Hello World
|
||||||
> These docs are currently a work in progress and are subject to change.
|
This is an example content file.
|
||||||
|
|
||||||
# <span className="flex gap-2.5 items-center"><Activity className="w-6 h-6 text-primary" /> Pulse App</span>
|
|
||||||
A lightweight service monitoring solution for tracking the availability of whatever service your heart desires!
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
Let's get you up and running—this will only take a few minutes! Start by [creating your account](#creating-your-account) and
|
|
||||||
making your first status page. You can do this on our cloud or on your [own instance](/self-hosting). Once done, you can add
|
|
||||||
your services and start monitoring them. See [Next Steps](#next-steps) for more.
|
|
||||||
|
|
||||||
### Creating your Account
|
|
||||||
- First, head to our [Dashboard](https://pulseapp.cc/dashboard) and fill out the form. ![Registering](https://cdn.rainnny.club/auhNOjrcYz6u.png)
|
|
||||||
- Next, complete the onboarding process and set up your first organization, and status page. ![Onboarding](https://cdn.rainnny.club/AfDgjHG5QTpZ.png)
|
|
||||||
- Finally, you can [start monitoring your services](#next-steps)!
|
|
@ -20,11 +20,12 @@
|
|||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.3",
|
"@radix-ui/react-tooltip": "^1.1.3",
|
||||||
|
"@vercel/og": "^0.6.3",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.0.0",
|
"cmdk": "1.0.4",
|
||||||
"framer-motion": "^11.11.1",
|
"framer-motion": "^11.11.1",
|
||||||
"lucide-react": "^0.447.0",
|
"lucide-react": "^0.452.0",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"next": "^15.0.0-canary.179",
|
"next": "^15.0.0-canary.179",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
@ -32,6 +33,7 @@
|
|||||||
"react-dom": "^19.0.0-rc-1460d67c-20241003",
|
"react-dom": "^19.0.0-rc-1460d67c-20241003",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remote-mdx": "^0.0.8",
|
"remote-mdx": "^0.0.8",
|
||||||
|
"simple-git": "^3.27.0",
|
||||||
"tailwind-merge": "^2.5.3",
|
"tailwind-merge": "^2.5.3",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
@ -41,7 +43,7 @@
|
|||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.14",
|
"eslint-config-next": "15.0.0",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
|
BIN
public/media/mike.png
Normal file
BIN
public/media/mike.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
@ -14,6 +14,7 @@ import { Metadata } from "next";
|
|||||||
import Embed from "@/components/embed";
|
import Embed from "@/components/embed";
|
||||||
import DocsFooter from "@/components/docs-footer";
|
import DocsFooter from "@/components/docs-footer";
|
||||||
import OnThisPage from "@/components/on-this-page";
|
import OnThisPage from "@/components/on-this-page";
|
||||||
|
import config from "@/config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The page to render the documentation markdown content.
|
* The page to render the documentation markdown content.
|
||||||
@ -30,11 +31,11 @@ const DocsPage = async ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Get the content to display based on the provided slug
|
// Get the content to display based on the provided slug
|
||||||
const pages: DocsContentMetadata[] = getDocsContent();
|
const pages: DocsContentMetadata[] = await getDocsContent();
|
||||||
const decodedSlug: string = decodeURIComponent(slug || "");
|
const decodedSlug: string = decodeURIComponent(slug || "");
|
||||||
const page: DocsContentMetadata | undefined = pages.find(
|
const page: DocsContentMetadata | undefined = pages.find(
|
||||||
(metadata: DocsContentMetadata): boolean =>
|
(metadata: DocsContentMetadata): boolean =>
|
||||||
metadata.slug === (decodedSlug || "intro")
|
metadata.slug === (decodedSlug || pages[0].slug)
|
||||||
);
|
);
|
||||||
if (!page) {
|
if (!page) {
|
||||||
notFound();
|
notFound();
|
||||||
@ -99,15 +100,19 @@ export const generateMetadata = async ({
|
|||||||
}): Promise<Metadata | undefined> => {
|
}): Promise<Metadata | undefined> => {
|
||||||
const slug: string = (((await params).slug as string[]) || undefined)?.join(
|
const slug: string = (((await params).slug as string[]) || undefined)?.join(
|
||||||
"/"
|
"/"
|
||||||
); // The slug of the content
|
);
|
||||||
if (slug) {
|
if (slug) {
|
||||||
const content: DocsContentMetadata | undefined = getDocsContent().find(
|
const pages: DocsContentMetadata[] = await getDocsContent();
|
||||||
(metadata: DocsContentMetadata): boolean => metadata.slug === slug
|
const decodedSlug: string = decodeURIComponent(slug || "");
|
||||||
); // Get the content based on the provided slug
|
const page: DocsContentMetadata | undefined = pages.find(
|
||||||
if (content) {
|
(metadata: DocsContentMetadata): boolean =>
|
||||||
|
metadata.slug === (decodedSlug || pages[0].slug)
|
||||||
|
);
|
||||||
|
if (page) {
|
||||||
return Embed({
|
return Embed({
|
||||||
title: content.title,
|
title: page.title,
|
||||||
description: content.summary,
|
description: page.summary,
|
||||||
|
thumbnail: config.ogApiUrl.replace("{title}", page.title),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
30
src/app/api/og/route.tsx
Normal file
30
src/app/api/og/route.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { ImageResponse } from "next/og";
|
||||||
|
import config from "@/config";
|
||||||
|
|
||||||
|
export const GET = async (request: Request) => {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const title: string | undefined = searchParams.has("title")
|
||||||
|
? searchParams.get("title")?.slice(0, 100)
|
||||||
|
: "Hello World (:";
|
||||||
|
|
||||||
|
return new ImageResponse(
|
||||||
|
(
|
||||||
|
<div tw="w-full h-full flex flex-col justify-center items-center bg-black/95 text-white">
|
||||||
|
{/* Logo */}
|
||||||
|
<img
|
||||||
|
src={(config.metadata.openGraph?.images as any)[0].url}
|
||||||
|
alt={`${config.siteName} Logo`}
|
||||||
|
width={96}
|
||||||
|
height={96}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h1 tw="text-5xl font-bold">{title}</h1>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
7
src/app/config.ts
Normal file
7
src/app/config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* The configuration for this app.
|
||||||
|
*/
|
||||||
|
import config from "@/configJson";
|
||||||
|
import { Config } from "@/types/config";
|
||||||
|
|
||||||
|
export default config as Config;
|
@ -7,56 +7,43 @@ import Sidebar from "@/components/sidebar/sidebar";
|
|||||||
import Footer from "@/components/footer";
|
import Footer from "@/components/footer";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { getDocsContent } from "@/lib/mdx";
|
import { getDocsContent } from "@/lib/mdx";
|
||||||
|
import config from "@/config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The metadata for this app.
|
* The metadata for this app.
|
||||||
*/
|
*/
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = config.metadata;
|
||||||
title: {
|
export const viewport: Viewport = config.viewport;
|
||||||
default: "Pulse Docs",
|
|
||||||
template: "%s • Pulse Docs",
|
export const dynamic = "force-dynamic";
|
||||||
},
|
|
||||||
description:
|
|
||||||
"A lightweight service monitoring solution for tracking the availability of whatever service your heart desires!",
|
|
||||||
openGraph: {
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: "https://pulseapp.cc/media/logo.png",
|
|
||||||
width: 128,
|
|
||||||
height: 128,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
card: "summary",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export const viewport: Viewport = {
|
|
||||||
themeColor: "#A855F7",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The primary layout for this app.
|
* The primary layout for this app.
|
||||||
*/
|
*/
|
||||||
const RootLayout = ({
|
const RootLayout = async ({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}>): ReactElement => {
|
}>): Promise<ReactElement> => {
|
||||||
const pages: DocsContentMetadata[] = getDocsContent();
|
const pages: DocsContentMetadata[] = await getDocsContent();
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
|
<body
|
||||||
<body
|
className="scroll-smooth antialiased"
|
||||||
className="scroll-smooth antialiased"
|
style={{
|
||||||
style={{
|
background: "var(--background-gradient)",
|
||||||
background: "var(--background-gradient)",
|
}}
|
||||||
}}
|
>
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="dark"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<TooltipProvider delayDuration={100}>
|
<TooltipProvider delayDuration={100}>
|
||||||
<div className="px-3 md:px-7 max-w-screen-2xl min-h-screen mx-auto flex flex-col transition-all">
|
<div className="px-3 md:px-7 max-w-screen-2xl min-h-screen mx-auto flex flex-col transition-transform">
|
||||||
<Navbar pages={pages} />
|
<Navbar pages={pages} />
|
||||||
<div className="pt-[4.5rem] w-full h-full flex flex-grow gap-5 sm:gap-8 transition-all transform-gpu">
|
<div className="pt-[4.5rem] w-full h-full flex flex-grow gap-5 sm:gap-8 transition-transform transform-gpu">
|
||||||
<div className="relative hidden xs:flex">
|
<div className="relative hidden xs:flex">
|
||||||
<Sidebar pages={pages} />
|
<Sidebar pages={pages} />
|
||||||
</div>
|
</div>
|
||||||
@ -65,8 +52,8 @@ const RootLayout = ({
|
|||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</body>
|
</ThemeProvider>
|
||||||
</ThemeProvider>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
33
src/app/not-found.tsx
Normal file
33
src/app/not-found.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { ReactElement } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The not found page.
|
||||||
|
*
|
||||||
|
* @return the page jsx
|
||||||
|
*/
|
||||||
|
const NotFoundPage = (): ReactElement => (
|
||||||
|
<main className="w-full pt-[25vh] min-h-screen flex justify-center select-none pointer-events-none">
|
||||||
|
<div className="h-fit flex gap-5 sm:gap-10 items-center transition-all transform-gpu">
|
||||||
|
{/* Image */}
|
||||||
|
<div className="relative w-24 h-24 xs:w-20 xs:h-20 sm:w-44 sm:h-44">
|
||||||
|
<Image
|
||||||
|
src="/media/mike.png"
|
||||||
|
alt="Mike Wazowski"
|
||||||
|
fill
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message */}
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<h1 className="text-3xl font-bold">Wrong Door!</h1>
|
||||||
|
<p className="max-w-72 sm:text-lg opacity-75">
|
||||||
|
The documentation page you were looking for could not be
|
||||||
|
found.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
export default NotFoundPage;
|
104
src/app/types/config.ts
Normal file
104
src/app/types/config.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import type { Metadata, Viewport } from "next";
|
||||||
|
|
||||||
|
export type Config = {
|
||||||
|
/**
|
||||||
|
* The name of this app.
|
||||||
|
*/
|
||||||
|
siteName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to use for generating OG images.
|
||||||
|
*/
|
||||||
|
ogApiUrl: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadata for this app.
|
||||||
|
*/
|
||||||
|
metadata: Metadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The viewport for this app.
|
||||||
|
*/
|
||||||
|
viewport: Viewport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The source to get the content from.
|
||||||
|
* This can either be a local source, or
|
||||||
|
* a remote Git repository.
|
||||||
|
*/
|
||||||
|
contentSource: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to link to for editing content of a page.
|
||||||
|
*/
|
||||||
|
contentEditUrl: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Social links for this app.
|
||||||
|
*/
|
||||||
|
socialLinks: SocialLinkType[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for the footer.
|
||||||
|
*/
|
||||||
|
footer: {
|
||||||
|
/**
|
||||||
|
* The URL to link to when the branding is clicked.
|
||||||
|
*/
|
||||||
|
homeUrl: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Links for the footer.
|
||||||
|
*/
|
||||||
|
links: {
|
||||||
|
[category: string]: FooterLink[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SocialLinkType = {
|
||||||
|
/**
|
||||||
|
* The name of this social link.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tooltip for this social link.
|
||||||
|
*/
|
||||||
|
tooltip: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The logo for this social link.
|
||||||
|
* This can either be an image URL, or
|
||||||
|
* the name of an icon from <a href="https://lucide.dev/icons">Lucide Icons</a>
|
||||||
|
*/
|
||||||
|
logo: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The href for this social link.
|
||||||
|
*/
|
||||||
|
href: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to show this social link in the navbar.
|
||||||
|
*/
|
||||||
|
navbar: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FooterLink = {
|
||||||
|
/**
|
||||||
|
* The name of this link.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The href for this link.
|
||||||
|
*/
|
||||||
|
href: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The optional name to show
|
||||||
|
* when the screen size is small.
|
||||||
|
*/
|
||||||
|
shortName?: string;
|
||||||
|
};
|
@ -17,7 +17,8 @@ const DocsFooter = ({
|
|||||||
|
|
||||||
const current: number = pages.findIndex(
|
const current: number = pages.findIndex(
|
||||||
(page: DocsContentMetadata) =>
|
(page: DocsContentMetadata) =>
|
||||||
(path === "/" && page.slug === "intro") || path === `/${page.slug}`
|
(path === "/" && page.slug === pages[0].slug) ||
|
||||||
|
path === `/${page.slug}`
|
||||||
);
|
);
|
||||||
const previous: DocsContentMetadata | undefined =
|
const previous: DocsContentMetadata | undefined =
|
||||||
current > 0 ? pages[current - 1] : undefined;
|
current > 0 ? pages[current - 1] : undefined;
|
||||||
|
@ -13,6 +13,11 @@ type EmbedProps = {
|
|||||||
* The description of the embed.
|
* The description of the embed.
|
||||||
*/
|
*/
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The optional thumbnail image of the embed.
|
||||||
|
*/
|
||||||
|
thumbnail?: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,13 +26,25 @@ type EmbedProps = {
|
|||||||
* @param props the embed props
|
* @param props the embed props
|
||||||
* @returns the embed jsx
|
* @returns the embed jsx
|
||||||
*/
|
*/
|
||||||
const Embed = ({ title, description }: EmbedProps): Metadata => {
|
const Embed = ({ title, description, thumbnail }: EmbedProps): Metadata => {
|
||||||
return {
|
return {
|
||||||
title: title,
|
title: title,
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `${title}`,
|
title: `${title}`,
|
||||||
description: description,
|
description: description,
|
||||||
|
...(thumbnail && {
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: thumbnail,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
|
...(thumbnail && {
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
},
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export default Embed;
|
export default Embed;
|
||||||
|
@ -5,45 +5,10 @@ import AnimatedGridPattern from "@/components/ui/animated-grid-pattern";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ExternalLink, Mail } from "lucide-react";
|
import { ExternalLink } from "lucide-react";
|
||||||
import SocialLink from "@/components/social-link";
|
import SocialLink from "@/components/social-link";
|
||||||
|
import config from "@/config";
|
||||||
const links = {
|
import { SocialLinkType } from "@/types/config";
|
||||||
Resources: [
|
|
||||||
{
|
|
||||||
name: "Support",
|
|
||||||
href: "https://support.pulseapp.cc",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Jobs",
|
|
||||||
href: "https://jobs.pulseapp.cc",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Developers",
|
|
||||||
shortName: "Devs",
|
|
||||||
href: "https://dev.pulseapp.cc",
|
|
||||||
external: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "System Status",
|
|
||||||
shortName: "Status",
|
|
||||||
href: "https://status.pulseapp.cc",
|
|
||||||
external: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
Legal: [
|
|
||||||
{
|
|
||||||
name: "Terms & Conditions",
|
|
||||||
shortName: "Terms",
|
|
||||||
href: "/legal/terms",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Privacy Policy",
|
|
||||||
shortName: "Privacy",
|
|
||||||
href: "/legal/privacy",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const Footer = (): ReactElement => (
|
const Footer = (): ReactElement => (
|
||||||
<footer className="relative mt-3 h-[19.5rem] md:h-[17rem] flex justify-center border-t border-zinc-700/75 overflow-hidden select-none">
|
<footer className="relative mt-3 h-[19.5rem] md:h-[17rem] flex justify-center border-t border-zinc-700/75 overflow-hidden select-none">
|
||||||
@ -55,46 +20,34 @@ const Footer = (): ReactElement => (
|
|||||||
|
|
||||||
{/* Socials */}
|
{/* Socials */}
|
||||||
<div className="pl-1 flex gap-2.5 items-center z-50">
|
<div className="pl-1 flex gap-2.5 items-center z-50">
|
||||||
<SocialLink
|
{config.socialLinks.map((link: SocialLinkType) => (
|
||||||
className="w-5 h-5"
|
<SocialLink
|
||||||
name="GitHub"
|
key={link.name}
|
||||||
tooltip="View our Github"
|
className="w-5 h-5"
|
||||||
logo="github.svg"
|
{...link}
|
||||||
href="https://github.com/PulseAppCC"
|
/>
|
||||||
/>
|
))}
|
||||||
<SocialLink
|
|
||||||
className="w-5 h-5"
|
|
||||||
name="Discord"
|
|
||||||
tooltip="Join our Discord"
|
|
||||||
logo="discord.svg"
|
|
||||||
href="https://discord.pulseapp.cc"
|
|
||||||
/>
|
|
||||||
<SocialLink
|
|
||||||
className="w-5 h-5"
|
|
||||||
name="Email"
|
|
||||||
tooltip="Email us"
|
|
||||||
logo={<Mail className="opacity-95 w-full h-full" />}
|
|
||||||
href="mailto:support@pulseapp.cc"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Links */}
|
{/* Links */}
|
||||||
<div className="flex gap-7 md:gap-12 transition-all transform-gpu">
|
<div className="flex gap-7 md:gap-12 transition-all transform-gpu">
|
||||||
{Object.entries(links).map(([title, links]) => (
|
{Object.entries(config.footer.links).map(
|
||||||
<LinkCategory key={title} title={title}>
|
([title, links]) => (
|
||||||
{links.map((link) => (
|
<LinkCategory key={title} title={title}>
|
||||||
<FooterLink key={link.name} {...link} />
|
{links.map((link) => (
|
||||||
))}
|
<FooterLink key={link.name} {...link} />
|
||||||
</LinkCategory>
|
))}
|
||||||
))}
|
</LinkCategory>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Copyright */}
|
{/* Copyright */}
|
||||||
<p className="absolute inset-x-0 bottom-3.5 flex text-sm text-center justify-center opacity-60">
|
<p className="absolute inset-x-0 bottom-3.5 flex text-sm text-center justify-center opacity-60">
|
||||||
Copyright © {new Date().getFullYear()} Pulse App. All
|
Copyright © {new Date().getFullYear()} {config.siteName}.
|
||||||
rights reserved.
|
All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -112,17 +65,17 @@ const Footer = (): ReactElement => (
|
|||||||
const Branding = () => (
|
const Branding = () => (
|
||||||
<Link
|
<Link
|
||||||
className="flex gap-3 items-center hover:opacity-75 transition-all transform-gpu"
|
className="flex gap-3 items-center hover:opacity-75 transition-all transform-gpu"
|
||||||
href="https://pulseapp.cc"
|
href={config.footer.homeUrl}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src="/media/logo.png"
|
src="/media/logo.png"
|
||||||
alt="Pulse App Logo"
|
alt={`${config.siteName} Logo`}
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
<h1 className="text-xl font-bold">Pulse App</h1>
|
<h1 className="text-xl font-bold">{config.siteName}</h1>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { capitalizeWords } from "@/lib/string";
|
import { capitalizeWords } from "@/lib/string";
|
||||||
import ImageViewer from "@/components/image-viewer";
|
import ImageViewer from "@/components/image-viewer";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
const blockquoteStyles: { [key: string]: any } = {
|
const blockquoteStyles: { [key: string]: any } = {
|
||||||
NOTE: {
|
NOTE: {
|
||||||
@ -96,10 +97,13 @@ const components = {
|
|||||||
// Media
|
// Media
|
||||||
img: ({ src, alt }: { src: string; alt: string }): ReactElement => (
|
img: ({ src, alt }: { src: string; alt: string }): ReactElement => (
|
||||||
<ImageViewer className="m-2 my-2.5">
|
<ImageViewer className="m-2 my-2.5">
|
||||||
<img
|
<Image
|
||||||
className="ring-1 ring-muted/45 rounded-2xl select-none"
|
className="ring-1 ring-muted/45 rounded-2xl select-none"
|
||||||
src={src}
|
src={src}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
|
width={1920}
|
||||||
|
height={1080}
|
||||||
|
unoptimized
|
||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
</ImageViewer>
|
</ImageViewer>
|
||||||
|
@ -6,6 +6,8 @@ import Image from "next/image";
|
|||||||
import QuickSearchDialog from "@/components/navbar/search-dialog";
|
import QuickSearchDialog from "@/components/navbar/search-dialog";
|
||||||
import Sidebar from "@/components/sidebar/sidebar";
|
import Sidebar from "@/components/sidebar/sidebar";
|
||||||
import SocialLink from "@/components/social-link";
|
import SocialLink from "@/components/social-link";
|
||||||
|
import config from "@/config";
|
||||||
|
import { SocialLinkType } from "@/types/config";
|
||||||
|
|
||||||
const Navbar = ({ pages }: { pages: DocsContentMetadata[] }): ReactElement => (
|
const Navbar = ({ pages }: { pages: DocsContentMetadata[] }): ReactElement => (
|
||||||
<nav className="fixed left-0 inset-x-0 bg-white/95 dark:bg-white/[0.007] backdrop-saturate-100 backdrop-blur-xl border-b z-50">
|
<nav className="fixed left-0 inset-x-0 bg-white/95 dark:bg-white/[0.007] backdrop-saturate-100 backdrop-blur-xl border-b z-50">
|
||||||
@ -19,7 +21,7 @@ const Navbar = ({ pages }: { pages: DocsContentMetadata[] }): ReactElement => (
|
|||||||
<h1 className="text-lg font-semibold">docs.</h1>
|
<h1 className="text-lg font-semibold">docs.</h1>
|
||||||
<Image
|
<Image
|
||||||
src="/media/logo.png"
|
src="/media/logo.png"
|
||||||
alt="Pulse App Logo"
|
alt={`${config.siteName} Logo`}
|
||||||
width={36}
|
width={36}
|
||||||
height={36}
|
height={36}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
@ -35,18 +37,11 @@ const Navbar = ({ pages }: { pages: DocsContentMetadata[] }): ReactElement => (
|
|||||||
|
|
||||||
{/* Social */}
|
{/* Social */}
|
||||||
<div className="flex gap-5 items-center">
|
<div className="flex gap-5 items-center">
|
||||||
<SocialLink
|
{config.socialLinks
|
||||||
name="GitHub"
|
.filter((link: SocialLinkType) => link.navbar)
|
||||||
tooltip="View our Github"
|
.map((link: SocialLinkType) => (
|
||||||
logo="github.svg"
|
<SocialLink key={link.name} {...link} />
|
||||||
href="https://github.com/PulseAppCC"
|
))}
|
||||||
/>
|
|
||||||
<SocialLink
|
|
||||||
name="Discord"
|
|
||||||
tooltip="Join our Discord"
|
|
||||||
logo="discord.svg"
|
|
||||||
href="https://discord.pulseapp.cc"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Sidebar */}
|
{/* Mobile Sidebar */}
|
||||||
|
@ -52,7 +52,7 @@ const QuickSearchDialog = ({
|
|||||||
className="cursor-pointer hover:opacity-85 transition-all transform-gpu select-none"
|
className="cursor-pointer hover:opacity-85 transition-all transform-gpu select-none"
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
>
|
>
|
||||||
<div className="absolute top-2.5 left-3 z-10">
|
<div className="absolute top-[0.55rem] left-3 z-10">
|
||||||
<Search className="w-[1.15rem] h-[1.15rem]" />
|
<Search className="w-[1.15rem] h-[1.15rem]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -8,6 +8,8 @@ import { motion, useInView } from "framer-motion";
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AlignLeftIcon, ArrowUpFromDot, MoveRight } from "lucide-react";
|
import { AlignLeftIcon, ArrowUpFromDot, MoveRight } from "lucide-react";
|
||||||
|
import config from "@/config";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
type Header = {
|
type Header = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -16,6 +18,7 @@ type Header = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const OnThisPage = ({ page }: { page: DocsContentMetadata }): ReactElement => {
|
const OnThisPage = ({ page }: { page: DocsContentMetadata }): ReactElement => {
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [headers, setHeaders] = useState<Header[]>([]);
|
const [headers, setHeaders] = useState<Header[]>([]);
|
||||||
const [activeHeader, setActiveHeader] = useState<string | undefined>(
|
const [activeHeader, setActiveHeader] = useState<string | undefined>(
|
||||||
undefined
|
undefined
|
||||||
@ -43,6 +46,7 @@ const OnThisPage = ({ page }: { page: DocsContentMetadata }): ReactElement => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setHeaders(extractedHeaders);
|
setHeaders(extractedHeaders);
|
||||||
|
setLoading(false);
|
||||||
}, [page.content]);
|
}, [page.content]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -80,7 +84,7 @@ const OnThisPage = ({ page }: { page: DocsContentMetadata }): ReactElement => {
|
|||||||
<motion.div
|
<motion.div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="sticky top-[7.5rem] w-44 max-h-[calc(100vh-3.5rem)] flex flex-col gap-2 text-sm select-none"
|
className="sticky top-[7.5rem] w-44 max-h-[calc(100vh-3.5rem)] flex flex-col gap-2 text-sm select-none"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 1 }}
|
||||||
animate={{ opacity: inView ? 1 : 0 }}
|
animate={{ opacity: inView ? 1 : 0 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
>
|
>
|
||||||
@ -92,37 +96,45 @@ const OnThisPage = ({ page }: { page: DocsContentMetadata }): ReactElement => {
|
|||||||
|
|
||||||
{/* Headers */}
|
{/* Headers */}
|
||||||
<ul className="relative">
|
<ul className="relative">
|
||||||
{headers.map((header: Header) => (
|
{loading ? (
|
||||||
<li
|
<Skeleton className="w-full h-5 bg-accent rounded-lg" />
|
||||||
key={header.id}
|
) : headers.length === 0 ? (
|
||||||
className={cn(
|
<span className="opacity-75">Nothing ):</span>
|
||||||
"hover:opacity-80 transition-all transform-gpu relative",
|
) : (
|
||||||
activeHeader === header.id
|
headers.map((header: Header) => (
|
||||||
? "font-semibold text-primary"
|
<li
|
||||||
: "opacity-65"
|
key={header.id}
|
||||||
)}
|
className={cn(
|
||||||
style={{ paddingLeft: `${(header.level - 1) * 16}px` }}
|
"hover:opacity-80 transition-all transform-gpu relative",
|
||||||
>
|
activeHeader === header.id
|
||||||
{/* Indentation */}
|
? "font-semibold text-primary"
|
||||||
{header.level > 1 && (
|
: "opacity-65"
|
||||||
<div
|
)}
|
||||||
className="absolute left-0 top-0 bottom-0 border-l border-muted"
|
style={{
|
||||||
style={{
|
paddingLeft: `${(header.level - 1) * 16}px`,
|
||||||
left: `${(header.level - 2) * 16 + 4}px`,
|
}}
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<Link
|
|
||||||
href={`#${header.id}`}
|
|
||||||
draggable={false}
|
|
||||||
className="block py-1"
|
|
||||||
>
|
>
|
||||||
{truncateText(header.text, 20)}
|
{/* Indentation */}
|
||||||
</Link>
|
{header.level > 1 && (
|
||||||
</li>
|
<div
|
||||||
))}
|
className="absolute left-0 top-0 bottom-0 border-l border-accent"
|
||||||
|
style={{
|
||||||
|
left: `${(header.level - 2) * 16 + 4}px`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<Link
|
||||||
|
href={`#${header.id}`}
|
||||||
|
draggable={false}
|
||||||
|
className="block py-1"
|
||||||
|
>
|
||||||
|
{truncateText(header.text, 20)}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
@ -148,7 +160,9 @@ const Footer = ({ page }: { page: DocsContentMetadata }): ReactElement => {
|
|||||||
{/* Edit on Git */}
|
{/* Edit on Git */}
|
||||||
<Link
|
<Link
|
||||||
className="flex gap-1.5 items-center text-xs hover:opacity-75 transition-all transform-gpu group"
|
className="flex gap-1.5 items-center text-xs hover:opacity-75 transition-all transform-gpu group"
|
||||||
href={`https://git.rainnny.club/PulseApp/docs/src/branch/master/docs/${page.slug}${page.extension}`}
|
href={config.contentEditUrl
|
||||||
|
.replace("{slug}", page.slug as string)
|
||||||
|
.replace("{ext}", page.extension as string)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
>
|
>
|
||||||
|
@ -22,7 +22,7 @@ const SidebarLinks = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{Object.values(tree).map((node: TreeNode) => (
|
{Object.values(tree).map((node: TreeNode) => (
|
||||||
<CategoryItem key={node.slug} node={node} />
|
<CategoryItem key={node.slug} pages={pages} node={node} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -36,17 +36,20 @@ type TreeNode = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CategoryItem = ({
|
const CategoryItem = ({
|
||||||
|
pages,
|
||||||
node,
|
node,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
isLast = true,
|
isLast = true,
|
||||||
}: {
|
}: {
|
||||||
|
pages: DocsContentMetadata[];
|
||||||
node: TreeNode;
|
node: TreeNode;
|
||||||
depth?: number;
|
depth?: number;
|
||||||
isLast?: boolean;
|
isLast?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const path = decodeURIComponent(usePathname());
|
const path = decodeURIComponent(usePathname());
|
||||||
const active =
|
const active =
|
||||||
(path === "/" && node.slug === "intro") || path === `/${node.slug}`;
|
(path === "/" && node.slug === pages[0].slug) ||
|
||||||
|
path === `/${node.slug}`;
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
const hasChildren = Object.keys(node.children).length > 0;
|
const hasChildren = Object.keys(node.children).length > 0;
|
||||||
|
|
||||||
@ -56,7 +59,7 @@ const CategoryItem = ({
|
|||||||
{depth > 0 && (
|
{depth > 0 && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute left-0 bottom-0 border-l border-muted",
|
"absolute left-0 bottom-0 border-l border-accent",
|
||||||
isLast ? "h-[32px]" : "h-[100%]",
|
isLast ? "h-[32px]" : "h-[100%]",
|
||||||
active && "border-primary"
|
active && "border-primary"
|
||||||
)}
|
)}
|
||||||
@ -72,7 +75,7 @@ const CategoryItem = ({
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
`relative w-full px-1.5 h-8 justify-between hover:bg-accent/10 dark:hover:bg-accent/35`,
|
`relative w-full px-1.5 h-8 justify-between hover:bg-accent/35 hover:opacity-90`,
|
||||||
node.isFolder
|
node.isFolder
|
||||||
? "mb-0.5 text-sm font-semibold"
|
? "mb-0.5 text-sm font-semibold"
|
||||||
: "lg:text-base",
|
: "lg:text-base",
|
||||||
@ -121,6 +124,7 @@ const CategoryItem = ({
|
|||||||
(child, index, array) => (
|
(child, index, array) => (
|
||||||
<CategoryItem
|
<CategoryItem
|
||||||
key={child.slug}
|
key={child.slug}
|
||||||
|
pages={pages}
|
||||||
node={child}
|
node={child}
|
||||||
depth={depth + 1}
|
depth={depth + 1}
|
||||||
isLast={index === array.length - 1}
|
isLast={index === array.length - 1}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import SidebarLinks from "@/components/sidebar/sidebar-links";
|
import SidebarLinks from "@/components/sidebar/sidebar-links";
|
||||||
import ThemeSwitcher from "@/components/theme-switcher";
|
|
||||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||||
import QuickSearchDialog from "@/components/navbar/search-dialog";
|
import QuickSearchDialog from "@/components/navbar/search-dialog";
|
||||||
import { AlignRightIcon } from "lucide-react";
|
import { AlignRightIcon } from "lucide-react";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import ThemeSwitcher from "@/components/theme-switcher";
|
||||||
|
|
||||||
const Sidebar = ({ pages }: { pages: DocsContentMetadata[] }): ReactElement => (
|
const Sidebar = ({ pages }: { pages: DocsContentMetadata[] }): ReactElement => (
|
||||||
<>
|
<>
|
||||||
@ -21,7 +21,7 @@ const Sidebar = ({ pages }: { pages: DocsContentMetadata[] }): ReactElement => (
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop */}
|
{/* Desktop */}
|
||||||
<div className="hidden xs:flex sticky top-[4.5rem] max-h-[calc(100vh-3.5rem)] overflow-y-auto min-w-32 w-40 lg:w-52 py-5 flex-col justify-between transition-all transform-gpu">
|
<div className="hidden xs:flex sticky top-[4.5rem] max-h-[calc(100vh-4.5rem)] min-w-32 w-40 lg:w-52 py-5 pb-3 flex-col justify-between transition-all transform-gpu overflow-y-auto">
|
||||||
<SidebarContent pages={pages} />
|
<SidebarContent pages={pages} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
import { ReactElement } from "react";
|
|
||||||
import SimpleTooltip from "@/components/simple-tooltip";
|
import SimpleTooltip from "@/components/simple-tooltip";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import Icon from "@/components/ui/icon";
|
||||||
|
import { icons } from "lucide-react";
|
||||||
|
import { SocialLinkType } from "@/types/config";
|
||||||
|
|
||||||
type SocialLinkProps = {
|
type SocialLinkProps = SocialLinkType & {
|
||||||
className?: string | undefined;
|
className?: string | undefined;
|
||||||
name: string;
|
|
||||||
tooltip: string;
|
|
||||||
logo: string | ReactElement;
|
|
||||||
href: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SocialLink = ({
|
const SocialLink = ({
|
||||||
@ -22,22 +20,25 @@ const SocialLink = ({
|
|||||||
<SimpleTooltip content={tooltip}>
|
<SimpleTooltip content={tooltip}>
|
||||||
<Link
|
<Link
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-6 h-6 hover:opacity-75 transition-all transform-gpu",
|
"w-6 h-6 hover:opacity-75 transition-all transform-gpu select-none",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
href={href}
|
href={href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
>
|
>
|
||||||
{typeof logo === "string" ? (
|
{logo.startsWith("./") ? (
|
||||||
<Image
|
<Image
|
||||||
src={`/media/${logo}`}
|
src={`/media/${logo.substring(2)}`}
|
||||||
alt={`${name}'s Logo`}
|
alt={`${name}'s Logo`}
|
||||||
fill
|
fill
|
||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
logo
|
<Icon
|
||||||
|
className="opacity-95 w-full h-full"
|
||||||
|
name={logo as keyof typeof icons}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</SimpleTooltip>
|
</SimpleTooltip>
|
||||||
|
@ -10,9 +10,9 @@ import SimpleTooltip from "@/components/simple-tooltip";
|
|||||||
import { capitalizeWords } from "@/lib/string";
|
import { capitalizeWords } from "@/lib/string";
|
||||||
|
|
||||||
const themes = {
|
const themes = {
|
||||||
|
system: <Monitor className="w-4 h-4" />,
|
||||||
dark: <MoonStar className="w-4 h-4" />,
|
dark: <MoonStar className="w-4 h-4" />,
|
||||||
light: <Sun className="w-4 h-4" />,
|
light: <Sun className="w-4 h-4" />,
|
||||||
system: <Monitor className="w-4 h-4" />,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,5 +54,4 @@ const ThemeSwitcher = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ThemeSwitcher;
|
export default ThemeSwitcher;
|
||||||
|
@ -83,14 +83,16 @@ export function GridPattern({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
let current = undefined;
|
||||||
|
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
resizeObserver.observe(containerRef.current);
|
resizeObserver.observe(containerRef.current);
|
||||||
|
current = containerRef.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (containerRef.current) {
|
if (current) {
|
||||||
resizeObserver.unobserve(containerRef.current);
|
resizeObserver.unobserve(current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [containerRef]);
|
}, [containerRef]);
|
||||||
|
10
src/components/ui/icon.tsx
Normal file
10
src/components/ui/icon.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { icons, LucideProps } from "lucide-react";
|
||||||
|
|
||||||
|
interface IconProps extends LucideProps {
|
||||||
|
name: keyof typeof icons;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Icon({ name, color, size, ...props }: IconProps) {
|
||||||
|
const LucideIcon = icons[name];
|
||||||
|
return <LucideIcon color={color} size={size} {...props} />;
|
||||||
|
}
|
15
src/components/ui/skeleton.tsx
Normal file
15
src/components/ui/skeleton.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Skeleton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton };
|
116
src/lib/mdx.ts
116
src/lib/mdx.ts
@ -1,6 +1,11 @@
|
|||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import { Stats } from "node:fs";
|
import { Stats } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import config from "@/config";
|
||||||
|
import simpleGit from "simple-git";
|
||||||
|
import os from "node:os";
|
||||||
|
import { cache } from "react";
|
||||||
|
import "server-only";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The regex to match for metadata.
|
* The regex to match for metadata.
|
||||||
@ -8,18 +13,85 @@ import path from "node:path";
|
|||||||
const METADATA_REGEX: RegExp = /---\s*([\s\S]*?)\s*---/;
|
const METADATA_REGEX: RegExp = /---\s*([\s\S]*?)\s*---/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The directory docs are stored in.
|
* Check if the DOCS_DIR is a Git URL.
|
||||||
*/
|
*/
|
||||||
const DOCS_DIR: string = path.join(process.cwd(), "docs");
|
const isGitUrl = (url: string): boolean => {
|
||||||
|
return /^https?:\/\/|git@|\.git$/.test(url);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the content to
|
* The directory docs are stored in.
|
||||||
* display in the docs.
|
|
||||||
*/
|
*/
|
||||||
export const getDocsContent = (): DocsContentMetadata[] => {
|
const DOCS_DIR: string = isGitUrl(config.contentSource)
|
||||||
|
? config.contentSource
|
||||||
|
: path.join(config.contentSource.replace("{process}", process.cwd()));
|
||||||
|
|
||||||
|
const LAST_UPDATE_FILE = path.join(
|
||||||
|
os.tmpdir(),
|
||||||
|
"docs_cache",
|
||||||
|
"last_update.json"
|
||||||
|
);
|
||||||
|
const CACHE_DURATION_MS: number = 5 * 60 * 1000; // 5 minutes in milliseconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clone the Git repository if DOCS_DIR is a URL, else use the local directory.
|
||||||
|
* If it's a Git URL, clone it to a cache directory and reuse it.
|
||||||
|
*/
|
||||||
|
const getDocsDirectory = cache(async (): Promise<string> => {
|
||||||
|
if (isGitUrl(DOCS_DIR)) {
|
||||||
|
const repoHash: string = Buffer.from(DOCS_DIR).toString("base64"); // Create a unique identifier based on the repo URL
|
||||||
|
const cacheDir: string = path.join(os.tmpdir(), "docs_cache", repoHash);
|
||||||
|
|
||||||
|
// Pull the latest changes from the repo if we don't have it
|
||||||
|
if (!fs.existsSync(cacheDir) || fs.readdirSync(cacheDir).length < 1) {
|
||||||
|
console.log("Cloning initial docs content from Git...");
|
||||||
|
try {
|
||||||
|
await simpleGit().clone(DOCS_DIR, cacheDir, { "--depth": 1 });
|
||||||
|
storeUpdatedRepoTime();
|
||||||
|
} catch (error) {
|
||||||
|
// Simply ignore this error. When cloning the repo for
|
||||||
|
// the first time, it'll sometimes error saying the dir
|
||||||
|
// is already created.
|
||||||
|
}
|
||||||
|
} else if (shouldUpdateRepo()) {
|
||||||
|
// Pull the latest changes from Git
|
||||||
|
console.log("Pulling docs content from Git...");
|
||||||
|
await simpleGit(cacheDir)
|
||||||
|
.reset(["--hard"]) // Reset any local changes
|
||||||
|
.pull(); // Pull latest changes
|
||||||
|
storeUpdatedRepoTime();
|
||||||
|
}
|
||||||
|
return cacheDir;
|
||||||
|
}
|
||||||
|
return DOCS_DIR;
|
||||||
|
});
|
||||||
|
|
||||||
|
const shouldUpdateRepo = (): boolean => {
|
||||||
|
if (!fs.existsSync(LAST_UPDATE_FILE)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
Date.now() -
|
||||||
|
JSON.parse(fs.readFileSync(LAST_UPDATE_FILE, "utf-8")).lastUpdate >
|
||||||
|
CACHE_DURATION_MS
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const storeUpdatedRepoTime = () =>
|
||||||
|
fs.writeFileSync(
|
||||||
|
LAST_UPDATE_FILE,
|
||||||
|
JSON.stringify({ lastUpdate: Date.now() }),
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the content to display in the docs.
|
||||||
|
*/
|
||||||
|
export const getDocsContent = async (): Promise<DocsContentMetadata[]> => {
|
||||||
|
const docsDir: string = await getDocsDirectory();
|
||||||
const content: DocsContentMetadata[] = [];
|
const content: DocsContentMetadata[] = [];
|
||||||
for (const directory of getRecursiveDirectories(DOCS_DIR)) {
|
for (const directory of getRecursiveDirectories(docsDir)) {
|
||||||
content.push(...getMetadata<DocsContentMetadata>(DOCS_DIR, directory));
|
content.push(...getMetadata<DocsContentMetadata>(docsDir, directory));
|
||||||
}
|
}
|
||||||
return content.sort((a: DocsContentMetadata, b: DocsContentMetadata) => {
|
return content.sort((a: DocsContentMetadata, b: DocsContentMetadata) => {
|
||||||
const orderA = a.order ?? Number.MAX_SAFE_INTEGER;
|
const orderA = a.order ?? Number.MAX_SAFE_INTEGER;
|
||||||
@ -45,18 +117,27 @@ const getMetadata = <T extends MDXMetadata>(
|
|||||||
const extension: string = path.extname(file); // The file extension
|
const extension: string = path.extname(file); // The file extension
|
||||||
return extension === ".md" || extension === ".mdx";
|
return extension === ".md" || extension === ".mdx";
|
||||||
}); // Read the MDX files
|
}); // Read the MDX files
|
||||||
return files.map((file: string): T => {
|
const metadata: T[] = [];
|
||||||
|
for (let i = files.length - 1; i >= 0; i--) {
|
||||||
|
const file: string = files[i];
|
||||||
const filePath: string = path.join(directory, file); // The path of the file
|
const filePath: string = path.join(directory, file); // The path of the file
|
||||||
return {
|
const fileMetadata: T | undefined = parseMetadata<T>(
|
||||||
|
fs.readFileSync(filePath, "utf-8")
|
||||||
|
);
|
||||||
|
if (!fileMetadata) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
metadata.push({
|
||||||
slug: filePath
|
slug: filePath
|
||||||
.replace(parent, "")
|
.replace(parent, "")
|
||||||
.replace(/\\/g, "/") // Normalize the path
|
.replace(/\\/g, "/") // Normalize the path
|
||||||
.replace(/\.mdx?$/, "")
|
.replace(/\.mdx?$/, "")
|
||||||
.substring(1),
|
.substring(1),
|
||||||
extension: path.extname(file),
|
extension: path.extname(file),
|
||||||
...parseMetadata<T>(fs.readFileSync(filePath, "utf-8")),
|
...fileMetadata,
|
||||||
}; // Map each file to its metadata
|
});
|
||||||
});
|
}
|
||||||
|
return metadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,8 +148,15 @@ const getMetadata = <T extends MDXMetadata>(
|
|||||||
* @returns the metadata and content
|
* @returns the metadata and content
|
||||||
* @template T the type of metadata
|
* @template T the type of metadata
|
||||||
*/
|
*/
|
||||||
const parseMetadata = <T extends MDXMetadata>(content: string): T => {
|
const parseMetadata = <T extends MDXMetadata>(
|
||||||
const metadataBlock: string = METADATA_REGEX.exec(content)![1]; // Get the block of metadata
|
content: string
|
||||||
|
): T | undefined => {
|
||||||
|
const extracted = METADATA_REGEX.exec(content);
|
||||||
|
const metadataBlock: string | undefined =
|
||||||
|
extracted && extracted.length > 1 ? extracted[1] : undefined; // Get the block of metadata
|
||||||
|
if (!metadataBlock) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
content = content.replace(METADATA_REGEX, "").trim(); // Remove the metadata block from the content
|
content = content.replace(METADATA_REGEX, "").trim(); // Remove the metadata block from the content
|
||||||
const metadata: Partial<{
|
const metadata: Partial<{
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
|
28
src/middleware.ts
Normal file
28
src/middleware.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { type NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export function middleware(request: NextRequest): NextResponse {
|
||||||
|
const before: number = Date.now();
|
||||||
|
const response: NextResponse = NextResponse.next();
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
const ip: string | null =
|
||||||
|
request.headers.get("CF-Connecting-IP") ||
|
||||||
|
request.headers.get("X-Forwarded-For");
|
||||||
|
console.log(
|
||||||
|
`${ip} | ${request.method} ${request.nextUrl.pathname} ${response.status} in ${(Date.now() - before).toFixed(0)}ms`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
/*
|
||||||
|
* Match all request paths except for the ones starting with:
|
||||||
|
* - api (API routes)
|
||||||
|
* - _next/static (static files)
|
||||||
|
* - _next/image (image optimization files)
|
||||||
|
* - favicon.ico (favicon file)
|
||||||
|
*/
|
||||||
|
"/((?!api|_next/static|_next/image|favicon.ico).*)",
|
||||||
|
],
|
||||||
|
};
|
@ -24,6 +24,15 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"./src/*"
|
"./src/*"
|
||||||
|
],
|
||||||
|
"@/config": [
|
||||||
|
"./src/app/config.ts"
|
||||||
|
],
|
||||||
|
"@/types/*": [
|
||||||
|
"./src/app/types/*"
|
||||||
|
],
|
||||||
|
"@/configJson": [
|
||||||
|
"./config.json"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"target": "ES2017"
|
"target": "ES2017"
|
||||||
|
Reference in New Issue
Block a user