1 Commits

Author SHA1 Message Date
Renovate Bot
d777ab2a64 Update dependency eslint-config-next to v14.2.14 2024-10-07 17:05:08 +00:00
45 changed files with 771 additions and 1093 deletions

View File

@ -1,56 +0,0 @@
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 }}

View File

@ -0,0 +1,31 @@
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
View File

@ -6,6 +6,7 @@ node_modules
.env*.local
next-env.d.ts
.sentryclirc
.env
sw.*
workbox-*
swe-worker-*

View File

@ -13,7 +13,7 @@ FROM base AS builder
WORKDIR /usr/src/app
COPY --from=depends /usr/src/app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV NEXT_TELEMETRY_DISABLED 1
RUN bun run build
@ -32,15 +32,14 @@ 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/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/docs ./docs
ENV NODE_ENV=production
ENV NODE_ENV production
# Exposting on port 80 so we can
# access via a reverse proxy for Dokku
ENV HOSTNAME="0.0.0.0"
ENV HOSTNAME "0.0.0.0"
EXPOSE 80
ENV PORT=80
ENV PORT 80
USER nextjs
CMD ["node", "server.js"]
CMD node server.js

View File

@ -1,2 +1,3 @@
# docs
The source coded for the Pulse App documentation site.
The public documentation for Pulse App.

BIN
bun.lockb

Binary file not shown.

View File

@ -1,90 +0,0 @@
{
"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"
}
]
}
}
}

View File

@ -1,10 +0,0 @@
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

7
docs/bob/hello.md Normal file
View File

@ -0,0 +1,7 @@
---
title: 'Hello'
published: '2024-10-06'
summary: 'petentium usu tota noluisse errem elaboraret auctor.'
---
# hello

7
docs/bob/hey.md Normal file
View File

@ -0,0 +1,7 @@
---
title: 'Hey'
published: '2024-10-06'
summary: 'petentium usu tota noluisse errem elaboraret auctor.'
---
# hey

7
docs/bob/hi.md Normal file
View File

@ -0,0 +1,7 @@
---
title: 'Hi'
published: '2024-10-06'
summary: 'petentium usu tota noluisse errem elaboraret auctor.'
---
# hi

293
docs/intro.md Normal file
View File

@ -0,0 +1,293 @@
---
title: '🚀 Introduction'
published: '2024-10-06'
summary: 'petentium usu tota noluisse errem elaboraret auctor.'
---
# Get started with Pulse App!
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
# an mel dissentiunt ponderum eius dicant adhuc,
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
# vim an explicari eirmod pro singulis scripta iaculis fermentum.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
# eruditi propriae vulputate elit venenatis reprehendunt delectus.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
# dicunt antiopam ultricies nisl egestas voluptatibus harum,
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
# viverra senserit cursus theophrastus elaboraret iudicabit ligula.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
# posidonium dicat eum nostra auctor quaeque harum
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
# doctus primis disputationi atqui magnis himenaeos fastidii
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
# ligula cras prodesset litora ridens docendi euripidis
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
# efficitur detraxit detraxit fames appareat mutat elit
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
# donec nominavi qui dolorum adversarium eum eleifend
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
# nunc contentiones numquam pharetra his vero solum
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.
petentium usu tota noluisse errem elaboraret auctor.

View File

@ -1,9 +0,0 @@
---
title: 'Example'
updated: '2024-10-06'
summary: 'petentium usu tota noluisse errem elaboraret auctor.'
order: 1
---
# Hello World
This is an example content file.

View File

@ -14,18 +14,18 @@
"lint": "next lint"
},
"dependencies": {
"@heroicons/react": "^2.1.5",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.3",
"@vercel/og": "^0.6.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"framer-motion": "^11.11.1",
"lucide-react": "^0.452.0",
"lucide-react": "^0.447.0",
"luxon": "^3.5.0",
"next": "^15.0.0-canary.179",
"next-themes": "^0.3.0",
@ -33,17 +33,16 @@
"react-dom": "^19.0.0-rc-1460d67c-20241003",
"remark-gfm": "^4.0.0",
"remote-mdx": "^0.0.8",
"simple-git": "^3.27.0",
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/luxon": "^3.4.2",
"@types/node": "^22.0.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "15.0.0",
"eslint-config-next": "14.2.14",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36">
<path fill="#5865f2"
<path fill="#fff"
d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 780 B

After

Width:  |  Height:  |  Size: 777 B

View File

@ -1,50 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 96 96" id="Github-Octocat--Streamline-Svg-Logos.svg"
height="98" width="98" stroke-width="1">
<desc>Github Octocat Streamline Icon: https://streamlinehq.com</desc>
<path fill="#9EDCF2"
d="M74.4304 73.4278c0 5.0396-11.6744 9.1002-26.068 9.1002-14.3936 0-26.068-4.0969-26.068-9.1002 0-5.0396 11.6744-9.1002 26.068-9.1002 14.3936 0 26.068 4.0606 26.068 9.1002Z"></path>
<mask id="a" width="30" height="25" x="33" y="70" maskUnits="userSpaceOnUse" style="mask-type:luminance">
<path fill="#fff"
d="M37.159 89.7428c.5801 2.7192 1.9941 4.3145 3.4081 5.2571h14.9012c1.8128-1.2327 3.6618-3.553 3.6618-7.9037V75.8568s.2175-2.7917 2.7917-3.6981c0 0 1.4865-1.0514-.1088-1.6315 0 0-7.0699-.5801-7.0699 5.2209v8.5564s.2901 3.1542-1.3777 4.4594V78.1772s.1088-3.3718 1.8491-4.6407c0 0 1.1602-2.0666-1.3777-1.5228 0 0-4.8583.6889-5.0759 6.381l-.1087 10.8768H47.492l-.1088-10.8768c-.2175-5.6559-5.0758-6.381-5.0758-6.381-2.5379-.5801-1.3777 1.5228-1.3777 1.5228 1.7402 1.2689 1.849 4.6407 1.849 4.6407v10.6955c-1.6678-1.1965-1.3777-4.5682-1.3777-4.5682v-8.5564c0-5.801-7.0699-5.2209-7.0699-5.2209-1.6315.5801-.1088 1.6315-.1088 1.6315 2.5379.9427 2.7917 3.6981 2.7917 3.6981v7.8676l.145 6.0184Z"></path>
</mask>
<g mask="url(#a)">
<path fill="#7DBCE7"
d="M74.4304 73.4276c0 5.0395-11.6744 9.1002-26.068 9.1002-14.3936 0-26.068-4.0969-26.068-9.1002 0-5.0396 11.6744-9.1003 26.068-9.1003 14.3936 0 26.068 4.0607 26.068 9.1003Z"></path>
</g>
<path fill="#9EDCF2"
d="m18.5959 46.6347-.7614 2.6104s-.1812.9427.6889 1.124c.9426-.0363.8701-.9064.7976-1.1602l-.7251-2.5742Z"></path>
<path fill="#010101"
d="m94.3348 35.6491.0725-.3263c-7.65-1.5227-15.4812-1.559-20.2308-1.3414.7614-2.7917 1.0152-6.0548 1.0152-9.6441 0-5.1846-1.9578-9.3177-5.0758-12.4358.5438-1.7765 1.2689-5.72838-.7251-10.76795 0 0-3.5531-1.12392813-11.6382 4.2782-3.1542-.79763-6.526-1.19645-9.8978-1.19645-3.6981 0-7.4325.47133-10.9493 1.41398C28.5667-.0628898 24.9048 1.0973 24.9048 1.0973c-2.3929 5.98222-.9064 10.4417-.4713 11.5294-2.828 3.0455-4.532 6.9248-4.532 11.6744 0 3.5893.3988 6.8161 1.414 9.6078-4.7858-.1813-12.32702-.1088-19.72321 1.3777l.07251.3263c7.39619-1.4865 15.0099-1.5228 19.7594-1.3415.2176.5801.4714 1.1602.7252 1.704-4.7133.1451-12.72585.7614-20.41209 2.9368l.10877.3263C9.60483 37.0631 17.6899 36.483 22.3669 36.338c2.828 5.2208 8.3389 8.6289 18.2004 9.6803-1.4139.9427-2.8279 2.5379-3.408 5.2571-1.9216.9064-7.94 3.1543-11.5656-3.0817 0 0-2.0304-3.6981-5.9097-3.9882 0 0-3.7706-.0725-.2538 2.3567 0 0 2.5016 1.1964 4.2419 5.6559 0 0 2.2841 7.6137 13.1971 5.1483v7.795s-.2175 2.7917-2.7917 3.6981c0 0-1.5227 1.0514.1088 1.6315 0 0 7.0699.5801 7.0699-5.2208v-8.5564s-.29-3.408 1.3777-4.5682v14.0672s-.1087 3.3718-1.849 4.6408c0 0-1.1602 2.0666 1.3777 1.5227 0 0 4.8583-.6888 5.0758-6.381l.1088-14.2485h1.1602l.1088 14.2485c.2175 5.6559 5.0758 6.381 5.0758 6.381 2.5379.5801 1.3777-1.5227 1.3777-1.5227-1.7403-1.269-1.8491-4.6408-1.8491-4.6408V52.2543c1.6678 1.3052 1.3778 4.4595 1.3778 4.4595v8.5564c0 5.8009 7.0699 5.2208 7.0699 5.2208 1.6315-.5801.1087-1.6315.1087-1.6315-2.5379-.9426-2.7917-3.6981-2.7917-3.6981V53.9221c0-4.387-1.849-6.7073-3.6618-7.9038 10.5142-1.0514 15.5538-4.4232 17.8741-9.7166 4.6045.1088 12.9071.6889 20.8109 2.9368l.1088-.3263c-7.8676-2.2116-16.0976-2.7917-20.7746-2.9368.2175-.5438.3988-1.0876.5801-1.6677 4.8582-.1813 12.7258-.1813 20.412 1.3414Z"></path>
<path fill="#F5CCB3"
d="M64.8947 24.0835c2.2479 2.0666 3.5893 4.532 3.5893 7.1787 0 12.472-9.2815 12.7983-20.7383 12.7983-11.4569 0-20.7384-1.7403-20.7384-12.7983 0-2.6467 1.3052-5.1121 3.5531-7.1424 3.7344-3.4081 10.0429-1.5953 17.1853-1.5953 7.1424 0 13.4147-1.849 17.149 1.559Z"></path>
<path fill="#fff"
d="M40.8579 31.9148c0 3.4443-1.9216 6.1997-4.3144 6.1997-2.3929 0-4.3145-2.7917-4.3145-6.1997 0-3.4443 1.9216-6.1998 4.3145-6.1998 2.3928-.0362 4.3144 2.7555 4.3144 6.1998Z"></path>
<path fill="#AF5C51"
d="M39.4794 31.9872c0 2.2842-1.3052 4.1332-2.8642 4.1332-1.5953 0-2.8642-1.849-2.8642-4.1332 0-2.2841 1.3052-4.1331 2.8642-4.1331s2.8642 1.849 2.8642 4.1331Z"></path>
<path fill="#fff"
d="M64.0254 31.9148c0 3.4443-1.9216 6.1997-4.3145 6.1997s-4.3144-2.7917-4.3144-6.1997c0-3.4443 1.9215-6.1998 4.3144-6.1998 2.3567-.0362 4.3145 2.7555 4.3145 6.1998Z"></path>
<path fill="#AF5C51"
d="M62.6108 31.9872c0 2.2842-1.3053 4.1332-2.8643 4.1332-1.5952 0-2.8642-1.849-2.8642-4.1332 0-2.2841 1.3052-4.1331 2.8642-4.1331 1.5953 0 2.8643 1.849 2.8643 4.1331Z"></path>
<path fill="#AF5C51"
d="M48.9068 37.4257c0 .5801-.4713 1.0877-1.0877 1.0877-.5801 0-1.0877-.4714-1.0877-1.0877 0-.6164.4714-1.0877 1.0877-1.0877.5801 0 1.0877.4713 1.0877 1.0877Z"></path>
<path fill="#AF5C51"
d="M45.3171 40.1449c-.0725-.1813.0363-.3625.2176-.4351.1813-.0725.3625.0363.4351.2176.29.7976 1.0151 1.3052 1.849 1.3052.8339 0 1.559-.5438 1.8491-1.3052.0725-.1813.2537-.2901.435-.2176.1813.0726.2901.2538.2176.4351-.3626 1.0514-1.3778 1.7766-2.5017 1.7766-1.1239 0-2.1391-.7252-2.5017-1.7766Z"></path>
<path fill="#C4E5D9"
d="M21.3518 45.0757c0 .29-.3263.5076-.7613.5076-.3988 0-.7614-.2176-.7614-.5076 0-.29.3263-.5076.7614-.5076.435 0 .7613.2176.7613.5076Z"></path>
<path fill="#C4E5D9"
d="M23.4544 46.2359c0 .29-.3263.5075-.7614.5075-.3988 0-.7614-.2175-.7614-.5075 0-.2901.3263-.5076.7614-.5076s.7614.2175.7614.5076Z"></path>
<path fill="#C4E5D9"
d="M24.7229 47.7586c0 .2901-.3263.5076-.7613.5076-.3988 0-.7614-.2175-.7614-.5076 0-.29.3263-.5076.7614-.5076.435-.0362.7613.2176.7613.5076Z"></path>
<path fill="#C4E5D9"
d="M25.8836 49.499c0 .29-.3263.5075-.7614.5075-.3988 0-.7614-.2175-.7614-.5075 0-.2901.3263-.5076.7614-.5076.4351-.0363.7614.2175.7614.5076Z"></path>
<path fill="#C4E5D9"
d="M27.1521 51.0941c0 .2901-.3263.5076-.7613.5076-.3988 0-.7614-.2175-.7614-.5076 0-.29.3263-.5076.7614-.5076.435 0 .7613.2176.7613.5076Z"></path>
<path fill="#C4E5D9"
d="M28.8924 52.5081c0 .2901-.3263.5076-.7614.5076-.3988 0-.7614-.2175-.7614-.5076 0-.29.3263-.5076.7614-.5076.4351-.0362.7614.2176.7614.5076Z"></path>
<path fill="#C4E5D9"
d="M31.3216 53.4146c0 .29-.3263.5075-.7614.5075-.3988 0-.7614-.2175-.7614-.5075 0-.2901.3263-.5076.7614-.5076s.7614.2175.7614.5076Z"></path>
<path fill="#C4E5D9"
d="M33.7517 53.4146c0 .29-.3263.5075-.7613.5075-.3988 0-.7614-.2175-.7614-.5075 0-.2901.3263-.5076.7614-.5076.435 0 .7613.2175.7613.5076Z"></path>
<path fill="#C4E5D9"
d="M36.2166 53.0157c0 .29-.3263.5076-.7614.5076-.3988 0-.7614-.2176-.7614-.5076 0-.29.3263-.5076.7614-.5076.3988 0 .7614.2176.7614.5076Z"></path>
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 986 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

View File

@ -13,8 +13,8 @@ import { capitalizeWords } from "@/lib/string";
import { Metadata } from "next";
import Embed from "@/components/embed";
import DocsFooter from "@/components/docs-footer";
import { cn } from "@/lib/utils";
import OnThisPage from "@/components/on-this-page";
import config from "@/config";
/**
* The page to render the documentation markdown content.
@ -31,11 +31,10 @@ const DocsPage = async ({
);
// Get the content to display based on the provided slug
const pages: DocsContentMetadata[] = await getDocsContent();
const decodedSlug: string = decodeURIComponent(slug || "");
const pages: DocsContentMetadata[] = getDocsContent();
const page: DocsContentMetadata | undefined = pages.find(
(metadata: DocsContentMetadata): boolean =>
metadata.slug === (decodedSlug || pages[0].slug)
metadata.slug === (slug || "intro")
);
if (!page) {
notFound();
@ -45,17 +44,20 @@ const DocsPage = async ({
return (
<main className="w-full flex flex-col">
{/* Breadcrumb */}
<Breadcrumb className="pt-4 pb-3 select-none">
<Breadcrumb className="pt-4 select-none">
<BreadcrumbList>
{splitSlug
.slice(0, -1)
.map((part: string, index: number): ReactElement => {
{splitSlug.map(
(part: string, index: number): ReactElement => {
const active: boolean =
index === splitSlug.length - 1;
const slug: string = splitSlug
.slice(1, index + 2) // Include one more to account for the index shift
.slice(1, index + 1)
.join("/");
return (
<div className="flex items-center" key={part}>
<BreadcrumbItem>
<BreadcrumbItem
className={cn(active && "text-primary")}
>
<BreadcrumbLink
href={slug}
draggable={false}
@ -63,17 +65,13 @@ const DocsPage = async ({
{capitalizeWords(part)}
</BreadcrumbLink>
</BreadcrumbItem>
{index < splitSlug.length - 1 && ( // Adjusted to avoid separator after the last breadcrumb
{index < splitSlug.length - 1 && (
<BreadcrumbSeparator className="pl-1.5" />
)}
</div>
);
})}
<BreadcrumbItem className="text-primary">
<BreadcrumbLink href="#" draggable={false}>
{page.title}{" "}
</BreadcrumbLink>
</BreadcrumbItem>
}
)}
</BreadcrumbList>
</Breadcrumb>
@ -100,19 +98,15 @@ export const generateMetadata = async ({
}): Promise<Metadata | undefined> => {
const slug: string = (((await params).slug as string[]) || undefined)?.join(
"/"
);
); // The slug of the content
if (slug) {
const pages: DocsContentMetadata[] = await getDocsContent();
const decodedSlug: string = decodeURIComponent(slug || "");
const page: DocsContentMetadata | undefined = pages.find(
(metadata: DocsContentMetadata): boolean =>
metadata.slug === (decodedSlug || pages[0].slug)
);
if (page) {
const content: DocsContentMetadata | undefined = getDocsContent().find(
(metadata: DocsContentMetadata): boolean => metadata.slug === slug
); // Get the content based on the provided slug
if (content) {
return Embed({
title: page.title,
description: page.summary,
thumbnail: config.ogApiUrl.replace("{title}", page.title),
title: content.title,
description: content.summary,
});
}
}

View File

@ -1,30 +0,0 @@
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,
}
);
};

View File

@ -1,7 +0,0 @@
/**
* The configuration for this app.
*/
import config from "@/configJson";
import { Config } from "@/types/config";
export default config as Config;

View File

@ -6,55 +6,64 @@ import Navbar from "@/components/navbar/navbar";
import Sidebar from "@/components/sidebar/sidebar";
import Footer from "@/components/footer";
import { TooltipProvider } from "@/components/ui/tooltip";
import { getDocsContent } from "@/lib/mdx";
import config from "@/config";
/**
* The metadata for this app.
*/
export const metadata: Metadata = config.metadata;
export const viewport: Viewport = config.viewport;
export const dynamic = "force-dynamic";
export const metadata: 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",
},
};
export const viewport: Viewport = {
themeColor: "#A855F7",
};
/**
* The primary layout for this app.
*/
const RootLayout = async ({
const RootLayout = ({
children,
}: Readonly<{
children: ReactNode;
}>): Promise<ReactElement> => {
const pages: DocsContentMetadata[] = await getDocsContent();
return (
<html lang="en">
<body
className="scroll-smooth antialiased"
style={{
background: "var(--background-gradient)",
}}
>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem
disableTransitionOnChange
>
<TooltipProvider delayDuration={100}>
<div className="px-3 md:px-7 max-w-screen-2xl min-h-screen mx-auto flex flex-col transition-transform">
<Navbar pages={pages} />
<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">
<Sidebar pages={pages} />
</div>
{children}
}>): ReactElement => (
<html lang="en">
<body
className="scroll-smooth antialiased"
style={{
background: "var(--background-gradient)",
}}
>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
<TooltipProvider delayDuration={100}>
<div className="px-7 max-w-[90rem] mx-auto min-h-screen flex flex-col">
<Navbar />
<div className="pt-[4.5rem] w-full h-full flex flex-grow gap-5">
<div className="relative hidden xs:flex">
<Sidebar />
</div>
{children}
</div>
<Footer />
</TooltipProvider>
</ThemeProvider>
</body>
</html>
);
};
</div>
<Footer />
</TooltipProvider>
</ThemeProvider>
</body>
</html>
);
export default RootLayout;

View File

@ -1,33 +0,0 @@
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;

View File

@ -20,13 +20,13 @@ body {
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 271 91% 65%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 0 0% 90%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;

View File

@ -1,104 +0,0 @@
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;
};

View File

@ -8,19 +8,14 @@ type DocsContentMetadata = MDXMetadata & {
title: string;
/**
* The date this content was updated.
* The date this content was published.
*/
updated: string;
published: string;
/**
* The summary of this content.
*/
summary: string;
/**
* The order of this content.
*/
order: number;
};
/**

View File

@ -3,36 +3,35 @@
import { ReactElement, useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import Link from "next/link";
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
import { Separator } from "@/components/ui/separator";
import { DateTime } from "luxon";
import SimpleTooltip from "@/components/simple-tooltip";
import { ChevronLeft, ChevronRight } from "lucide-react";
const DocsFooter = ({
pages,
}: {
pages: DocsContentMetadata[];
}): ReactElement => {
const path: string = decodeURIComponent(usePathname());
const path: string = usePathname();
const current: number = pages.findIndex(
(page: DocsContentMetadata) =>
(path === "/" && page.slug === pages[0].slug) ||
path === `/${page.slug}`
(path === "/" && page.slug === "intro") || path === `/${page.slug}`
);
const previous: DocsContentMetadata | undefined =
current > 0 ? pages[current - 1] : undefined;
const next: DocsContentMetadata | undefined =
current < pages.length - 1 ? pages[current + 1] : undefined;
const [updatedDate, setUpdatedDate] = useState<string | null>(
DateTime.fromISO(pages[current]?.updated).toRelative()
const [publicationDate, setPublicationDate] = useState<string | null>(
DateTime.fromISO(pages[current]?.published).toRelative()
);
useEffect(() => {
const interval = setInterval(() => {
setUpdatedDate(
DateTime.fromISO(pages[current]?.updated).toRelative()
setPublicationDate(
DateTime.fromISO(pages[current]?.published).toRelative()
);
}, 1000);
return () => clearInterval(interval);
@ -40,24 +39,22 @@ const DocsFooter = ({
return (
<footer className="xs:mx-5 sm:mx-10 my-2 flex flex-col select-none transition-all transform-gpu">
{/* updated Date */}
{/* Publish Date */}
<div className="ml-auto pt-4">
<SimpleTooltip
content={`Last updated on ${DateTime.fromISO(
pages[current]?.updated
).toLocaleString(DateTime.DATETIME_MED)}`}
content={DateTime.fromISO(
pages[current]?.published
).toLocaleString(DateTime.DATETIME_MED)}
>
<span className="text-xs sm:text-sm opacity-75 transition-all transform-gpu">
Updated {updatedDate}
<span className="text-sm opacity-75">
Published {publicationDate}
</span>
</SimpleTooltip>
</div>
{/* Pages */}
{previous || next ? (
<Separator className="my-4 dark:bg-separator-gradient" />
) : undefined}
<div className="flex justify-between text-xs sm:text-base">
<Separator className="my-4" />
<div className="flex justify-between">
{/* Previous */}
{previous && (
<Link
@ -65,7 +62,7 @@ const DocsFooter = ({
href={`/${previous.slug}` || "#"}
draggable={false}
>
<ChevronLeft className="pb-1 w-4 h-4 group-hover:-translate-x-0.5 transition-all transform-gpu" />
<ChevronLeftIcon className="pb-1 w-4 h-4 group-hover:-translate-x-0.5 transition-all transform-gpu" />
<div className="flex flex-col">
<h1 className="text-sm opacity-75">Previous</h1>
<p>{previous.title}</p>
@ -84,7 +81,7 @@ const DocsFooter = ({
<h1 className="text-sm opacity-75">Next</h1>
<p>{next.title}</p>
</div>
<ChevronRight className="pb-1 w-4 h-4 group-hover:translate-x-0.5 transition-all transform-gpu" />
<ChevronRightIcon className="pb-1 w-4 h-4 group-hover:translate-x-0.5 transition-all transform-gpu" />
</Link>
)}
</div>

View File

@ -13,11 +13,6 @@ type EmbedProps = {
* The description of the embed.
*/
description: string;
/**
* The optional thumbnail image of the embed.
*/
thumbnail?: string | undefined;
};
/**
@ -26,25 +21,13 @@ type EmbedProps = {
* @param props the embed props
* @returns the embed jsx
*/
const Embed = ({ title, description, thumbnail }: EmbedProps): Metadata => {
const Embed = ({ title, description }: EmbedProps): Metadata => {
return {
title: title,
openGraph: {
title: `${title}`,
description: description,
...(thumbnail && {
images: [
{
url: thumbnail,
},
],
}),
},
...(thumbnail && {
twitter: {
card: "summary_large_image",
},
}),
};
};
export default Embed;

View File

@ -4,14 +4,51 @@ import { ReactElement, ReactNode } from "react";
import AnimatedGridPattern from "@/components/ui/animated-grid-pattern";
import Link from "next/link";
import Image from "next/image";
import {
ArrowTopRightOnSquareIcon,
EnvelopeIcon,
} from "@heroicons/react/24/outline";
import { cn } from "@/lib/utils";
import { ExternalLink } from "lucide-react";
import SocialLink from "@/components/social-link";
import config from "@/config";
import { SocialLinkType } from "@/types/config";
const 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",
},
],
};
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="mt-3 relative h-[19.5rem] md:h-[17rem] flex justify-center border-t border-zinc-700/75 overflow-hidden select-none">
<div className="w-full md:max-w-[65rem]">
<div className="px-5 py-5 md:py-10 w-full flex flex-col md:flex-row items-center justify-around md:items-start gap-7">
{/* Top */}
@ -20,34 +57,42 @@ const Footer = (): ReactElement => (
{/* Socials */}
<div className="pl-1 flex gap-2.5 items-center z-50">
{config.socialLinks.map((link: SocialLinkType) => (
<SocialLink
key={link.name}
className="w-5 h-5"
{...link}
/>
))}
<SocialLink
name="GitHub"
logo="github.svg"
href="https://github.com/PulseAppCC"
/>
<SocialLink
name="Discord"
logo="discord.svg"
href="https://discord.pulseapp.cc"
/>
<SocialLink
name="Email"
logo={
<EnvelopeIcon className="opacity-95 w-6 h-6" />
}
href="mailto:support@pulseapp.cc"
/>
</div>
</div>
{/* Links */}
<div className="flex gap-7 md:gap-12 transition-all transform-gpu">
{Object.entries(config.footer.links).map(
([title, links]) => (
<LinkCategory key={title} title={title}>
{links.map((link) => (
<FooterLink key={link.name} {...link} />
))}
</LinkCategory>
)
)}
{Object.entries(links).map(([title, links]) => (
<LinkCategory key={title} title={title}>
{links.map((link) => (
<FooterLink key={link.name} {...link} />
))}
</LinkCategory>
))}
</div>
</div>
{/* Copyright */}
<p className="absolute inset-x-0 bottom-3.5 flex text-sm text-center justify-center opacity-60">
Copyright &copy; {new Date().getFullYear()} {config.siteName}.
All rights reserved.
Copyright &copy; {new Date().getFullYear()} Pulse App. All
rights reserved.
</p>
</div>
@ -65,17 +110,46 @@ const Footer = (): ReactElement => (
const Branding = () => (
<Link
className="flex gap-3 items-center hover:opacity-75 transition-all transform-gpu"
href={config.footer.homeUrl}
href="https://pulseapp.cc"
draggable={false}
>
<Image
src="/media/logo.png"
alt={`${config.siteName} Logo`}
alt="Pulse App Logo"
width={40}
height={40}
draggable={false}
/>
<h1 className="text-xl font-bold">{config.siteName}</h1>
<h1 className="text-xl font-bold">Pulse App</h1>
</Link>
);
const SocialLink = ({
name,
logo,
href,
}: {
name: string;
logo: string | ReactElement;
href: string;
}) => (
<Link
className="hover:opacity-75 transition-all transform-gpu"
href={href}
target="_blank"
draggable={false}
>
{typeof logo === "string" ? (
<Image
src={`/media/${logo}`}
alt={`${name}'s Logo`}
width={20}
height={20}
draggable={false}
/>
) : (
logo
)}
</Link>
);
@ -113,7 +187,7 @@ const FooterLink = ({
{name}
</span>
{shortName && <span className="flex sm:hidden">{shortName}</span>}
{external && <ExternalLink className="w-3.5 h-3.5" />}
{external && <ArrowTopRightOnSquareIcon className="w-3.5 h-3.5" />}
</Link>
);
};

View File

@ -1,32 +0,0 @@
"use client";
import { ReactElement, ReactNode } from "react";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
type ImageViewerProps = {
className?: string | undefined;
children: ReactNode;
};
const ImageViewer = ({
className,
children,
}: ImageViewerProps): ReactElement => {
return (
<Dialog>
<DialogTrigger
className={cn(
"hover:scale-[1.005] transition-all transform-gpu",
className
)}
>
{children}
</DialogTrigger>
<DialogContent className="p-0 min-w-[20rem] max-w-screen-xl">
{children}
</DialogContent>
</Dialog>
);
};
export default ImageViewer;

View File

@ -1,48 +1,12 @@
import { isValidElement, ReactElement, ReactNode } from "react";
import { ReactElement, ReactNode } from "react";
import { MDXRemote } from "remote-mdx/rsc";
import { cn } from "@/lib/utils";
import remarkGfm from "remark-gfm";
import {
Activity,
CircleAlert,
Lightbulb,
MessageSquareWarning,
OctagonAlert,
TriangleAlert,
} from "lucide-react";
import Link from "next/link";
import { capitalizeWords } from "@/lib/string";
import ImageViewer from "@/components/image-viewer";
import Image from "next/image";
const blockquoteStyles: { [key: string]: any } = {
NOTE: {
icon: <CircleAlert className="w-4 h-4" />,
style: "text-[#1F6FEB] border-[#1F6FEB]",
},
TIP: {
icon: <Lightbulb className="w-4 h-4" />,
style: "text-[#4A8BD5] border-[#4A8BD5]",
},
IMPORTANT: {
icon: <MessageSquareWarning className="w-4 h-4" />,
style: "text-[#8957E5] border-[#8957E5]",
},
WARNING: {
icon: <TriangleAlert className="w-4 h-4" />,
style: "text-[#9E6A03] border-[#9E6A03]",
},
CAUTION: {
icon: <OctagonAlert className="w-4 h-4" />,
style: "text-[#DA3633] border-[#DA3633]",
},
};
/**
* The MDX components to style.
*/
const components = {
// Headings
h1: ({ children }: { children: ReactNode }): ReactElement => (
<Heading as="h1" size={1} className="text-4xl">
{children}
@ -73,8 +37,6 @@ const components = {
{children}
</Heading>
),
// Text
a: ({
href,
children,
@ -82,62 +44,19 @@ const components = {
href: string;
children: ReactNode;
}): ReactElement => (
<Link
className="text-primary cursor-pointer hover:opacity-75 transition-all transform-gpu"
<a
className="text-minecraft-green-4 cursor-pointer hover:opacity-85 transition-all transform-gpu"
href={href}
draggable={false}
>
{children}
</Link>
</a>
),
p: ({ children }: { children: ReactNode }): ReactElement => (
<p className="leading-5 select-none">{children}</p>
<p className="leading-4 text-zinc-300/80">{children}</p>
),
// Media
img: ({ src, alt }: { src: string; alt: string }): ReactElement => (
<ImageViewer className="m-2 my-2.5">
<Image
className="ring-1 ring-muted/45 rounded-2xl select-none"
src={src}
alt={alt}
width={1920}
height={1080}
unoptimized
draggable={false}
/>
</ImageViewer>
),
// Lists
ul: ({ children }: { children: ReactNode }): ReactElement => (
<ul className="px-3 list-disc list-inside select-none">{children}</ul>
<ul className="px-3 list-disc list-inside">{children}</ul>
),
// Blockquotes
blockquote: ({ children }: { children: ReactNode }): ReactElement => {
const match = extractBlockQuoteText(children).match(
/^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)]\s*(.*)/i
);
let style: any;
if (!match || !(style = blockquoteStyles[match[1]])) {
return <blockquote>{children}</blockquote>;
}
return (
<blockquote
className={cn(
"my-2 pl-3 py-1.5 flex flex-col gap-2 border-l-[3px] select-none",
style.style
)}
>
<h1 className="flex gap-2 items-center">
{style.icon}
{capitalizeWords(match[1])}
</h1>
<p className="text-foreground opacity-85">{match[2]}</p>
</blockquote>
);
},
};
/**
@ -152,8 +71,6 @@ export const CustomMDX = (props: any): ReactElement => (
components={{
...components,
...(props.components || {}),
Link,
Activity,
}}
options={{
mdxOptions: {
@ -166,7 +83,6 @@ export const CustomMDX = (props: any): ReactElement => (
/**
* A heading component.
*
* @param as the type of heading
* @param className the class name of the heading
* @param size the size of the heading
* @param children the children within the heading
@ -188,11 +104,7 @@ const Heading = ({
return (
<Component
id={id}
className={cn(
"py-3 font-bold select-none",
size >= 2 && "pt-7",
className
)}
className={cn("pt-2.5 font-bold", size >= 2 && "pt-7", className)}
>
{children}
</Component>
@ -205,16 +117,3 @@ const slugify = (text: string): string =>
.replace(/[^\w\s-]/g, "")
.replace(/[\s_-]+/g, "-")
.trim();
const extractBlockQuoteText = (node: ReactNode): string => {
if (typeof node === "string") {
return node;
}
if (Array.isArray(node)) {
return node.map(extractBlockQuoteText).join("");
}
if (isValidElement(node)) {
return extractBlockQuoteText(node.props.children);
}
return "";
};

View File

@ -1,55 +1,76 @@
"use client";
import { ReactElement } from "react";
import Link from "next/link";
import Image from "next/image";
import QuickSearchDialog from "@/components/navbar/search-dialog";
import { getDocsContent } from "@/lib/mdx";
import Sidebar from "@/components/sidebar/sidebar";
import SocialLink from "@/components/social-link";
import config from "@/config";
import { SocialLinkType } from "@/types/config";
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">
<div className="px-3 md:px-7 max-w-screen-2xl mx-auto py-4 flex justify-between items-center transition-all transform-gpu">
{/* Branding */}
<Link
className="flex gap-1 items-end hover:opacity-75 transition-all transform-gpu select-none"
href="/"
draggable={false}
>
<h1 className="text-lg font-semibold">docs.</h1>
<Image
src="/media/logo.png"
alt={`${config.siteName} Logo`}
width={36}
height={36}
const Navbar = (): ReactElement => {
const pages: DocsContentMetadata[] = getDocsContent();
return (
<nav className="fixed left-0 inset-x-0 bg-white/[0.007] backdrop-saturate-100 backdrop-blur-xl border-b z-50">
<div className="px-7 max-w-[90rem] mx-auto py-4 flex justify-between items-center">
{/* Branding */}
<Link
className="flex gap-1 items-end hover:opacity-75 transition-all transform-gpu select-none"
href="/"
draggable={false}
/>
</Link>
>
<h1 className="text-lg font-semibold">docs.</h1>
<Image
src="/media/logo.png"
alt="Pulse App Logo"
width={36}
height={36}
draggable={false}
/>
</Link>
{/* Right */}
<div className="flex gap-5 sm:gap-7 items-center transition-all transform-gpu">
{/* Search */}
<div className="hidden xs:flex">
<QuickSearchDialog pages={pages} bindKeybind />
</div>
{/* Right */}
<div className="flex gap-5 sm:gap-7 items-center transition-all transform-gpu">
{/* Search */}
<div className="hidden xs:flex">
<QuickSearchDialog pages={pages} bindKeybind />
</div>
{/* Social */}
<div className="flex gap-5 items-center">
{config.socialLinks
.filter((link: SocialLinkType) => link.navbar)
.map((link: SocialLinkType) => (
<SocialLink key={link.name} {...link} />
))}
</div>
{/* Social */}
<div className="flex gap-5 items-center">
<SocialLink
name="GitHub"
link="https://github.com/PulseAppCC"
icon="/media/github.svg"
/>
<SocialLink
name="Discord"
link="https://discord.pulseapp.cc"
icon="/media/discord.svg"
/>
</div>
{/* Mobile Sidebar */}
<div className="flex xs:hidden">
<Sidebar pages={pages} />
{/* Mobile Sidebar */}
<div className="flex xs:hidden">
<Sidebar />
</div>
</div>
</div>
</div>
</nav>
</nav>
);
};
const SocialLink = ({
name,
link,
icon,
}: {
name: string;
link: string;
icon: string;
}): ReactElement => (
<div className="relative w-6 h-6 hover:opacity-75 transition-all transform-gpu select-none">
<Link href={link} target="_blank" draggable={false}>
<Image src={icon} alt={`${name} Logo`} fill draggable={false} />
</Link>
</div>
);
export default Navbar;

View File

@ -9,10 +9,10 @@ import {
CommandItem,
CommandList,
} from "@/components/ui/command";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { Input } from "@/components/ui/input";
import { useRouter } from "next/navigation";
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
import { Search } from "lucide-react";
/**
* The dialog for quickly searching the docs.
@ -52,8 +52,8 @@ const QuickSearchDialog = ({
className="cursor-pointer hover:opacity-85 transition-all transform-gpu select-none"
onClick={() => setOpen(true)}
>
<div className="absolute top-[0.55rem] left-3 z-10">
<Search className="w-[1.15rem] h-[1.15rem]" />
<div className="absolute top-2.5 left-3 z-10">
<MagnifyingGlassIcon className="w-[1.15rem] h-[1.15rem]" />
</div>
<Input
@ -114,4 +114,5 @@ const QuickSearchDialog = ({
</>
);
};
export default QuickSearchDialog;

View File

@ -1,15 +1,17 @@
"use client";
import { ReactElement, useEffect, useRef, useState } from "react";
import {
ArrowLongRightIcon,
ArrowLongUpIcon,
Bars3CenterLeftIcon,
} from "@heroicons/react/24/outline";
import Link from "next/link";
import { cn } from "@/lib/utils";
import { truncateText } from "@/lib/string";
import { motion, useInView } from "framer-motion";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
import { AlignLeftIcon, ArrowUpFromDot, MoveRight } from "lucide-react";
import config from "@/config";
import { Skeleton } from "@/components/ui/skeleton";
type Header = {
id: string;
@ -18,7 +20,6 @@ type Header = {
};
const OnThisPage = ({ page }: { page: DocsContentMetadata }): ReactElement => {
const [loading, setLoading] = useState<boolean>(true);
const [headers, setHeaders] = useState<Header[]>([]);
const [activeHeader, setActiveHeader] = useState<string | undefined>(
undefined
@ -36,7 +37,7 @@ const OnThisPage = ({ page }: { page: DocsContentMetadata }): ReactElement => {
let match;
while ((match = headerRegex.exec(page.content)) !== null) {
const level: number = match[1].length; // The number of # symbols determines the header level
const text: string = match[2].trim().replace(/<[^>]*>/g, "");
const text: string = match[2].trim();
const id: string = text
.toLowerCase()
.replace(/\s+/g, "-")
@ -46,7 +47,6 @@ const OnThisPage = ({ page }: { page: DocsContentMetadata }): ReactElement => {
}
setHeaders(extractedHeaders);
setLoading(false);
}, [page.content]);
useEffect(() => {
@ -83,63 +83,55 @@ const OnThisPage = ({ page }: { page: DocsContentMetadata }): ReactElement => {
return (
<motion.div
ref={ref}
className="sticky top-[7.5rem] w-44 max-h-[calc(100vh-3.5rem)] flex flex-col gap-2 text-sm select-none"
initial={{ opacity: 1 }}
className="sticky top-[5.5rem] w-44 max-h-[calc(100vh-3.5rem)] flex flex-col gap-2 text-sm select-none"
initial={{ opacity: 0 }}
animate={{ opacity: inView ? 1 : 0 }}
transition={{ duration: 0.2 }}
>
{/* Title */}
<div className="flex gap-2.5 items-center">
<AlignLeftIcon className="w-5 h-5" />
<Bars3CenterLeftIcon className="w-5 h-5" />
<h1>On This Page</h1>
</div>
{/* Headers */}
<ul className="relative">
{loading ? (
<Skeleton className="w-full h-5 bg-accent rounded-lg" />
) : headers.length === 0 ? (
<span className="opacity-75">Nothing ):</span>
) : (
headers.map((header: Header) => (
<li
key={header.id}
className={cn(
"hover:opacity-80 transition-all transform-gpu relative",
activeHeader === header.id
? "font-semibold text-primary"
: "opacity-65"
)}
style={{
paddingLeft: `${(header.level - 1) * 16}px`,
}}
>
{/* Indentation */}
{header.level > 1 && (
<div
className="absolute left-0 top-0 bottom-0 border-l border-accent"
style={{
left: `${(header.level - 2) * 16 + 4}px`,
}}
/>
)}
{headers.map((header: Header) => (
<li
key={header.id}
className={cn(
"hover:opacity-80 transition-all transform-gpu relative",
activeHeader === header.id
? "font-semibold text-primary"
: "opacity-65"
)}
style={{ paddingLeft: `${(header.level - 1) * 16}px` }}
>
{/* Indentation */}
{header.level > 1 && (
<div
className="absolute left-0 top-0 bottom-0 border-l border-muted"
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>
))
)}
{/* Header */}
<Link
href={`#${header.id}`}
draggable={false}
className="block py-1"
>
{truncateText(header.text, 24)}
</Link>
</li>
))}
</ul>
{/* Footer */}
<div>
<Separator className="mt-1 mb-3.5 dark:bg-separator-gradient" />
<Separator className="mt-1 mb-3.5" />
<Footer page={page} />
</div>
</motion.div>
@ -160,14 +152,12 @@ const Footer = ({ page }: { page: DocsContentMetadata }): ReactElement => {
{/* Edit on Git */}
<Link
className="flex gap-1.5 items-center text-xs hover:opacity-75 transition-all transform-gpu group"
href={config.contentEditUrl
.replace("{slug}", page.slug as string)
.replace("{ext}", page.extension as string)}
href={`https://git.rainnny.club/PulseApp/docs/src/branch/master/docs/${page.slug}.md`}
target="_blank"
draggable={false}
>
<span>Edit this page on GitHub</span>
<MoveRight className="w-4 h-4 group-hover:translate-x-px transition-all transform-gpu" />
<ArrowLongRightIcon className="w-4 h-4 group-hover:translate-x-0.5 transition-all transform-gpu" />
</Link>
{/* Scroll to Top */}
@ -187,7 +177,7 @@ const Footer = ({ page }: { page: DocsContentMetadata }): ReactElement => {
}
>
<span>Scroll to Top</span>
<ArrowUpFromDot className="w-4 h-4 group-hover:-translate-y-px transition-all transform-gpu" />
<ArrowLongUpIcon className="w-4 h-4 group-hover:translate-x-0.5 transition-all transform-gpu" />
</Button>
</div>
</footer>

View File

@ -11,7 +11,7 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "framer-motion";
import { ChevronRight } from "lucide-react";
import { ChevronRightIcon } from "@heroicons/react/24/outline";
const SidebarLinks = ({
pages,
@ -20,11 +20,11 @@ const SidebarLinks = ({
}): ReactElement => {
const tree = useMemo(() => buildTree(pages), [pages]);
return (
<div className="flex flex-col gap-1">
<>
{Object.values(tree).map((node: TreeNode) => (
<CategoryItem key={node.slug} pages={pages} node={node} />
<CategoryItem key={node.slug} node={node} />
))}
</div>
</>
);
};
@ -36,33 +36,29 @@ type TreeNode = {
};
const CategoryItem = ({
pages,
node,
depth = 0,
isLast = true,
}: {
pages: DocsContentMetadata[];
node: TreeNode;
depth?: number;
isLast?: boolean;
}) => {
const path = decodeURIComponent(usePathname());
const path = usePathname();
const active =
(path === "/" && node.slug === pages[0].slug) ||
path === `/${node.slug}`;
(path === "/" && node.slug === "intro") || path === `/${node.slug}`;
const [isOpen, setIsOpen] = useState(true);
const hasChildren = Object.keys(node.children).length > 0;
return (
<div className={cn(`relative select-none`, depth > 0 && "ml-3")}>
<div className={`relative ${depth > 0 ? "ml-2.5" : ""} select-none`}>
{/* Indentation */}
{depth > 0 && (
<div
className={cn(
"absolute left-0 bottom-0 border-l border-accent",
isLast ? "h-[32px]" : "h-[100%]",
active && "border-primary"
)}
className={`absolute left-0 top-1 bottom-0 border-l-2 border-muted`}
style={{
height: isLast ? "30px" : "100%",
}}
/>
)}
@ -75,13 +71,9 @@ const CategoryItem = ({
>
<Button
className={cn(
`relative w-full px-1.5 h-8 justify-between hover:bg-accent/35 hover:opacity-90`,
node.isFolder
? "mb-0.5 text-sm font-semibold"
: "lg:text-base",
depth > 0 && "pl-4",
`relative px-1.5 ${depth > 0 ? "pl-4" : ""} w-full justify-between`,
active &&
"text-primary/95 font-semibold hover:text-primary"
"bg-primary/15 hover:bg-primary/20 text-primary/95 hover:text-primary"
)}
variant="ghost"
>
@ -89,10 +81,10 @@ const CategoryItem = ({
{hasChildren && (
<motion.div
initial={false}
animate={{ rotate: isOpen ? 90 : 0 }}
animate={{ rotate: isOpen ? 90 : 180 }}
transition={{ duration: 0.2 }}
>
<ChevronRight className="w-4 h-4" />
<ChevronRightIcon className="w-4 h-4" />
</motion.div>
)}
</Button>
@ -124,7 +116,6 @@ const CategoryItem = ({
(child, index, array) => (
<CategoryItem
key={child.slug}
pages={pages}
node={child}
depth={depth + 1}
isLast={index === array.length - 1}
@ -143,7 +134,7 @@ const CategoryItem = ({
const buildTree = (pages: DocsContentMetadata[]): Record<string, TreeNode> => {
const tree: Record<string, TreeNode> = {};
pages.forEach((page: DocsContentMetadata) => {
pages.forEach((page) => {
const parts: string[] | undefined = page.slug?.split("/");
let currentLevel = tree;

View File

@ -1,52 +1,52 @@
import { ReactElement } from "react";
import { Separator } from "@/components/ui/separator";
import { getDocsContent } from "@/lib/mdx";
import SidebarLinks from "@/components/sidebar/sidebar-links";
import ThemeSwitcher from "@/components/theme-switcher";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import QuickSearchDialog from "@/components/navbar/search-dialog";
import { AlignRightIcon } from "lucide-react";
import { Separator } from "@/components/ui/separator";
import ThemeSwitcher from "@/components/theme-switcher";
import { Bars3BottomRightIcon } from "@heroicons/react/24/outline";
const Sidebar = ({ pages }: { pages: DocsContentMetadata[] }): ReactElement => (
const Sidebar = (): ReactElement => (
<>
{/* Mobile */}
<div className="xs:hidden">
<Sheet>
<SheetTrigger className="flex items-center">
<AlignRightIcon className="w-6 h-6" />
<Bars3BottomRightIcon className="w-6 h-6" />
</SheetTrigger>
<SheetContent className="h-full px-5 pt-11" side="right">
<SidebarContent pages={pages} />
<SheetContent className="h-full px-11 pt-10" side="top">
<SidebarContent />
</SheetContent>
</Sheet>
</div>
{/* Desktop */}
<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} />
<div className="hidden xs:flex sticky top-[4.3rem] 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">
<SidebarContent />
</div>
</>
);
const SidebarContent = ({
pages,
}: {
pages: DocsContentMetadata[];
}): ReactElement => (
<div className="h-full flex flex-col justify-between">
{/* Top */}
<div className="flex flex-col">
<div className="xs:hidden pb-3">
<QuickSearchDialog pages={pages} />
const SidebarContent = (): ReactElement => {
const pages: DocsContentMetadata[] = getDocsContent();
return (
<div className="h-full flex flex-col justify-between">
{/* Top */}
<div className="flex flex-col">
<div className="xs:hidden pb-3">
<QuickSearchDialog pages={pages} />
</div>
<SidebarLinks pages={pages} />
</div>
<SidebarLinks pages={pages} />
</div>
{/* Theme Switcher */}
<div className="flex flex-col items-center">
<Separator className="mb-3 dark:bg-separator-gradient" />
<ThemeSwitcher />
{/* Theme Switcher */}
<div className="flex flex-col">
<Separator className="mb-3" />
<ThemeSwitcher />
</div>
</div>
</div>
);
);
};
export default Sidebar;

View File

@ -39,10 +39,7 @@ const SimpleTooltip = ({
}: SimpleTooltipProps): ReactElement => (
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent
className="bg-accent text-accent-foreground"
side={side}
>
<TooltipContent className="bg-muted text-white" side={side}>
{content}
</TooltipContent>
</Tooltip>

View File

@ -1,46 +0,0 @@
import SimpleTooltip from "@/components/simple-tooltip";
import Link from "next/link";
import Image from "next/image";
import { cn } from "@/lib/utils";
import Icon from "@/components/ui/icon";
import { icons } from "lucide-react";
import { SocialLinkType } from "@/types/config";
type SocialLinkProps = SocialLinkType & {
className?: string | undefined;
};
const SocialLink = ({
className,
name,
tooltip,
logo,
href,
}: SocialLinkProps) => (
<SimpleTooltip content={tooltip}>
<Link
className={cn(
"w-6 h-6 hover:opacity-75 transition-all transform-gpu select-none",
className
)}
href={href}
target="_blank"
draggable={false}
>
{logo.startsWith("./") ? (
<Image
src={`/media/${logo.substring(2)}`}
alt={`${name}'s Logo`}
fill
draggable={false}
/>
) : (
<Icon
className="opacity-95 w-full h-full"
name={logo as keyof typeof icons}
/>
)}
</Link>
</SimpleTooltip>
);
export default SocialLink;

View File

@ -1,20 +1,13 @@
"use client";
import { ReactElement, useEffect, useState } from "react";
import { MoonStar, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { UseThemeProps } from "next-themes/dist/types";
import { Monitor, MoonStar, Sun } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import SimpleTooltip from "@/components/simple-tooltip";
import { motion } from "framer-motion";
import { UseThemeProps } from "next-themes/dist/types";
import { capitalizeWords } from "@/lib/string";
const themes = {
system: <Monitor className="w-4 h-4" />,
dark: <MoonStar className="w-4 h-4" />,
light: <Sun className="w-4 h-4" />,
};
/**
* The theme switcher component.
*
@ -22,36 +15,48 @@ const themes = {
*/
const ThemeSwitcher = (): ReactElement => {
const [mounted, setMounted] = useState(false);
const { theme: activeTheme, setTheme }: UseThemeProps = useTheme();
const { theme, setTheme }: UseThemeProps = useTheme();
const isLight = theme === "light";
useEffect(() => {
setMounted(true);
}, []);
return (
<div className="w-fit p-1 flex gap-1.5 bg-gray-600/5 dark:bg-black/30 ring-1 light:ring-inset ring-gray-600/5 dark:ring-white/5 rounded-full">
{Object.entries(themes).map(([theme, icon]) => {
const active: boolean = mounted && theme === activeTheme;
return (
<SimpleTooltip
key={theme}
content={`${capitalizeWords(theme)} Theme`}
>
<Button
className={cn(
"p-1 h-6 opacity-80 rounded-full",
active &&
"ring-1 bg-white dark:bg-zinc-900 ring-gray-900/10 dark:ring-white/15 hover:bg-white hover:dark:bg-zinc-900 opacity-100"
)}
variant="ghost"
onClick={() => setTheme(theme)}
>
{icon}
</Button>
</SimpleTooltip>
);
})}
</div>
return mounted ? (
<Button
className="p-1.5 flex gap-7 justify-start items-center hover:opacity-85 select-none"
variant="ghost"
onClick={() => setTheme(isLight ? "dark" : "light")}
>
<div className="relative flex items-center">
<motion.div
className="absolute"
initial={{ rotate: 0, scale: 1 }}
animate={{
rotate: isLight ? 0 : -90,
scale: isLight ? 1 : 0,
}}
transition={{ duration: 0.5 }}
>
<Sun className="w-[1.1rem] h-[1.1rem]" />
</motion.div>
<motion.div
className="absolute"
initial={{ rotate: 90, scale: 0 }}
animate={{
rotate: isLight ? 90 : 0,
scale: isLight ? 0 : 1,
}}
transition={{ duration: 0.5 }}
>
<MoonStar className="w-[1.1rem] h-[1.1rem]" />
</motion.div>
</div>
<span>{capitalizeWords(theme)}</span>
</Button>
) : (
<></>
);
};
export default ThemeSwitcher;

View File

@ -70,8 +70,7 @@ export function GridPattern({
if (dimensions.width && dimensions.height) {
setSquares(generateSquares(numSquares));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dimensions, numSquares]);
}, [dimensions, numSquares, generateSquares]);
// Resize observer to update container dimensions
useEffect(() => {
@ -83,16 +82,14 @@ export function GridPattern({
});
}
});
let current = undefined;
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
current = containerRef.current;
}
return () => {
if (current) {
resizeObserver.unobserve(current);
if (containerRef.current) {
resizeObserver.unobserve(containerRef.current);
}
};
}, [containerRef]);

View File

@ -1,10 +0,0 @@
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} />;
}

View File

@ -1,15 +0,0 @@
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 };

View File

@ -1,103 +1,27 @@
import * as fs from "node:fs";
import { Stats } from "node:fs";
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.
*/
const METADATA_REGEX: RegExp = /---\s*([\s\S]*?)\s*---/;
/**
* Check if the DOCS_DIR is a Git URL.
*/
const isGitUrl = (url: string): boolean => {
return /^https?:\/\/|git@|\.git$/.test(url);
};
/**
* The directory docs are stored in.
*/
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
const DOCS_DIR: string = path.join(process.cwd(), "docs");
/**
* 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.
* Get the content to
* display in the docs.
*/
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();
export const getDocsContent = (): DocsContentMetadata[] => {
const content: DocsContentMetadata[] = [];
for (const directory of getRecursiveDirectories(docsDir)) {
content.push(...getMetadata<DocsContentMetadata>(docsDir, directory));
for (const directory of getRecursiveDirectories(DOCS_DIR)) {
content.push(...getMetadata<DocsContentMetadata>(DOCS_DIR, directory));
}
return content.sort((a: DocsContentMetadata, b: DocsContentMetadata) => {
const orderA = a.order ?? Number.MAX_SAFE_INTEGER;
const orderB = b.order ?? Number.MAX_SAFE_INTEGER;
return orderA - orderB;
});
return content;
};
/**
@ -117,27 +41,18 @@ const getMetadata = <T extends MDXMetadata>(
const extension: string = path.extname(file); // The file extension
return extension === ".md" || extension === ".mdx";
}); // Read the MDX files
const metadata: T[] = [];
for (let i = files.length - 1; i >= 0; i--) {
const file: string = files[i];
return files.map((file: string): T => {
const filePath: string = path.join(directory, file); // The path of the file
const fileMetadata: T | undefined = parseMetadata<T>(
fs.readFileSync(filePath, "utf-8")
);
if (!fileMetadata) {
continue;
}
metadata.push({
return {
slug: filePath
.replace(parent, "")
.replace(/\\/g, "/") // Normalize the path
.replace(/\.mdx?$/, "")
.substring(1),
extension: path.extname(file),
...fileMetadata,
});
}
return metadata;
...parseMetadata<T>(fs.readFileSync(filePath, "utf-8")),
}; // Map each file to its metadata
});
};
/**
@ -148,15 +63,8 @@ const getMetadata = <T extends MDXMetadata>(
* @returns the metadata and content
* @template T the type of metadata
*/
const parseMetadata = <T extends MDXMetadata>(
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;
}
const parseMetadata = <T extends MDXMetadata>(content: string): T => {
const metadataBlock: string = METADATA_REGEX.exec(content)![1]; // Get the block of metadata
content = content.replace(METADATA_REGEX, "").trim(); // Remove the metadata block from the content
const metadata: Partial<{
[key: string]: string;

View File

@ -1,28 +0,0 @@
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).*)",
],
};

View File

@ -63,10 +63,6 @@ const config: Config = {
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
backgroundImage: {
"separator-gradient":
"linear-gradient(90deg, #161619, hsl(var(--muted)), #161619)",
},
},
},
plugins: [require("tailwindcss-animate")],

View File

@ -24,15 +24,6 @@
"paths": {
"@/*": [
"./src/*"
],
"@/config": [
"./src/app/config.ts"
],
"@/types/*": [
"./src/app/types/*"
],
"@/configJson": [
"./config.json"
]
},
"target": "ES2017"