Compare commits
37 Commits
5559a36de0
...
v1.0.4
| Author | SHA1 | Date | |
|---|---|---|---|
| fd8f068594 | |||
| 00bafa761b | |||
| d0d66dd85f | |||
| 6f5c9bd613 | |||
| 9f6168592c | |||
| 29d474a102 | |||
| a31202f63b | |||
| 0afd6bbb60 | |||
| 2c647f0284 | |||
| d9ff6d640d | |||
| 8ab9ec7d1f | |||
| 0cc67d54ef | |||
| cbb95a38cf | |||
| 5b163d6d74 | |||
| f6e774b5c9 | |||
| 613c8b1645 | |||
| 9e1aae5d76 | |||
| f1e3ad1357 | |||
| 39b044c2c2 | |||
| c0c73315c8 | |||
| 72fbae0666 | |||
| 3ed32210ad | |||
| f2366b5a38 | |||
| dccf6ad2ce | |||
| 788c9ca7ac | |||
| 34474de163 | |||
| 12646e45e4 | |||
| b25299a3a8 | |||
| aa9b280f5c | |||
| 2ec9a29565 | |||
| 20cafce97d | |||
| 31f931f7ce | |||
| e415b5118b | |||
| 84aef6b860 | |||
| 195932dde4 | |||
| 977773fe94 | |||
| a5e2e5a2db |
@@ -80,5 +80,5 @@ SENTRY_DSN=
|
||||
# GOTIFY_TOKEN=
|
||||
|
||||
# Analytics (Umami)
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
|
||||
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
|
||||
UMAMI_WEBSITE_ID=
|
||||
UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
|
||||
|
||||
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
@@ -16,6 +16,8 @@ jobs:
|
||||
prepare:
|
||||
name: 🔍 Prepare Environment
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
outputs:
|
||||
target: ${{ steps.determine.outputs.target }}
|
||||
image_tag: ${{ steps.determine.outputs.image_tag }}
|
||||
@@ -24,8 +26,26 @@ jobs:
|
||||
next_public_base_url: ${{ steps.determine.outputs.next_public_base_url }}
|
||||
directus_url: ${{ steps.determine.outputs.directus_url }}
|
||||
directus_host: ${{ steps.determine.outputs.directus_host }}
|
||||
gatekeeper_host: ${{ steps.determine.outputs.gatekeeper_host }}
|
||||
traefik_rule: ${{ steps.determine.outputs.traefik_rule }}
|
||||
gatekeeper_rule: ${{ steps.determine.outputs.gatekeeper_rule }}
|
||||
traefik_middlewares: ${{ steps.determine.outputs.traefik_middlewares }}
|
||||
project_name: ${{ steps.determine.outputs.project_name }}
|
||||
steps:
|
||||
- name: 🔍 Debug Info
|
||||
shell: bash
|
||||
run: |
|
||||
echo "ref_name: ${{ github.ref_name }}"
|
||||
echo "ref_type: ${{ github.ref_type }}"
|
||||
echo "tag: ${{ github.ref_name }}"
|
||||
|
||||
- name: 🧹 Maintenance (Runner Cleanup)
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
run: |
|
||||
docker image prune -f || true
|
||||
docker builder prune -f --filter "until=24h" || true
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -33,36 +53,87 @@ jobs:
|
||||
|
||||
- name: 🔍 Determine Environment
|
||||
id: determine
|
||||
shell: bash
|
||||
run: |
|
||||
TAG="${{ github.ref_name }}"
|
||||
REF="${{ github.ref }}"
|
||||
REF_NAME="${{ github.ref_name }}"
|
||||
REF_TYPE="${{ github.ref_type }}"
|
||||
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
|
||||
DOMAIN_BASE="mb-grid-solutions.com"
|
||||
PRJ_ID="mb-grid-solutions"
|
||||
|
||||
if [[ "${{ github.ref_type }}" == "branch" && "$TAG" == "main" ]]; then
|
||||
TARGET="staging"
|
||||
IMAGE_TAG="staging-${SHORT_SHA}"
|
||||
ENV_FILE=".env.staging"
|
||||
TRAEFIK_HOST="\`staging.${DOMAIN_BASE}\`"
|
||||
NEXT_PUBLIC_BASE_URL="https://staging.${DOMAIN_BASE}"
|
||||
DIRECTUS_URL="https://cms.staging.${DOMAIN_BASE}"
|
||||
DIRECTUS_HOST="\`cms.staging.${DOMAIN_BASE}\`"
|
||||
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
TARGET="production"
|
||||
IMAGE_TAG="$TAG"
|
||||
ENV_FILE=".env.prod"
|
||||
TRAEFIK_HOST="\`${DOMAIN_BASE}\`, \`www.${DOMAIN_BASE}\`"
|
||||
NEXT_PUBLIC_BASE_URL="https://${DOMAIN_BASE}"
|
||||
DIRECTUS_URL="https://cms.${DOMAIN_BASE}"
|
||||
DIRECTUS_HOST="\`cms.${DOMAIN_BASE}\`"
|
||||
else
|
||||
TARGET="skip"
|
||||
echo "Detecting environment for ref: $REF ($REF_NAME, type: $REF_TYPE)"
|
||||
|
||||
# Fallback for REF_TYPE if missing
|
||||
if [[ -z "$REF_TYPE" ]]; then
|
||||
if [[ "$REF" == refs/tags/* ]]; then
|
||||
REF_TYPE="tag"
|
||||
elif [[ "$REF" == refs/heads/* ]]; then
|
||||
REF_TYPE="branch"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$REF_TYPE" == "branch" && "$REF_NAME" == "main" ]]; then
|
||||
TARGET="testing"
|
||||
IMAGE_TAG="testing-${SHORT_SHA}"
|
||||
ENV_FILE=".env.testing"
|
||||
TRAEFIK_HOST="testing.${DOMAIN_BASE}"
|
||||
GATEKEEPER_HOST="gatekeeper.testing.${DOMAIN_BASE}"
|
||||
NEXT_PUBLIC_BASE_URL="https://testing.${DOMAIN_BASE}"
|
||||
DIRECTUS_URL="https://cms.testing.${DOMAIN_BASE}"
|
||||
DIRECTUS_HOST="cms.testing.${DOMAIN_BASE}"
|
||||
elif [[ "$REF_TYPE" == "tag" ]]; then
|
||||
if [[ "$REF_NAME" =~ ^v[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
|
||||
TARGET="production"
|
||||
IMAGE_TAG="$REF_NAME"
|
||||
ENV_FILE=".env.prod"
|
||||
TRAEFIK_HOST="${DOMAIN_BASE}" # Primary domain
|
||||
GATEKEEPER_HOST="gatekeeper.${DOMAIN_BASE}"
|
||||
NEXT_PUBLIC_BASE_URL="https://${DOMAIN_BASE}"
|
||||
DIRECTUS_URL="https://cms.${DOMAIN_BASE}"
|
||||
DIRECTUS_HOST="cms.${DOMAIN_BASE}"
|
||||
elif [[ "$REF_NAME" =~ -rc || "$REF_NAME" =~ -beta || "$REF_NAME" =~ -alpha ]]; then
|
||||
TARGET="staging"
|
||||
IMAGE_TAG="$REF_NAME"
|
||||
ENV_FILE=".env.staging"
|
||||
TRAEFIK_HOST="staging.${DOMAIN_BASE}"
|
||||
GATEKEEPER_HOST="gatekeeper.staging.${DOMAIN_BASE}"
|
||||
NEXT_PUBLIC_BASE_URL="https://staging.${DOMAIN_BASE}"
|
||||
DIRECTUS_URL="https://cms.staging.${DOMAIN_BASE}"
|
||||
DIRECTUS_HOST="cms.staging.${DOMAIN_BASE}"
|
||||
else
|
||||
TARGET="skip"
|
||||
echo "Tag $REF_NAME did not match any environment pattern."
|
||||
fi
|
||||
else
|
||||
TARGET="skip"
|
||||
echo "Ref type $REF_TYPE is not handled for deployment."
|
||||
fi
|
||||
|
||||
# Determine Rules based on target (if not skipped)
|
||||
if [[ "$TARGET" != "skip" ]]; then
|
||||
if [[ "$TARGET" == "production" ]]; then
|
||||
TRAEFIK_RULE="Host(\`${DOMAIN_BASE}\`) || Host(\`www.${DOMAIN_BASE}\`)"
|
||||
GATEKEEPER_RULE="(Host(\`${DOMAIN_BASE}\`) || Host(\`www.${DOMAIN_BASE}\`)) && PathPrefix(\`/gatekeeper\`) || Host(\`gatekeeper.${DOMAIN_BASE}\`)"
|
||||
TRAEFIK_MIDDLEWARES="compress"
|
||||
else
|
||||
TRAEFIK_RULE="Host(\`${TRAEFIK_HOST}\`)"
|
||||
GATEKEEPER_RULE="(Host(\`${TRAEFIK_HOST}\`) && PathPrefix(\`/gatekeeper\`)) || Host(\`gatekeeper.${TRAEFIK_HOST}\`)"
|
||||
TRAEFIK_MIDDLEWARES="${PRJ_ID}-${TARGET}-auth"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Target determined: $TARGET"
|
||||
echo "Image tag: $IMAGE_TAG"
|
||||
|
||||
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
|
||||
echo "image_tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
|
||||
echo "env_file=$ENV_FILE" >> "$GITHUB_OUTPUT"
|
||||
echo "traefik_host=$TRAEFIK_HOST" >> "$GITHUB_OUTPUT"
|
||||
echo "traefik_rule=$TRAEFIK_RULE" >> "$GITHUB_OUTPUT"
|
||||
echo "gatekeeper_rule=$GATEKEEPER_RULE" >> "$GITHUB_OUTPUT"
|
||||
echo "traefik_middlewares=$TRAEFIK_MIDDLEWARES" >> "$GITHUB_OUTPUT"
|
||||
echo "gatekeeper_host=$GATEKEEPER_HOST" >> "$GITHUB_OUTPUT"
|
||||
echo "next_public_base_url=$NEXT_PUBLIC_BASE_URL" >> "$GITHUB_OUTPUT"
|
||||
echo "directus_url=$DIRECTUS_URL" >> "$GITHUB_OUTPUT"
|
||||
echo "directus_host=$DIRECTUS_HOST" >> "$GITHUB_OUTPUT"
|
||||
@@ -73,18 +144,30 @@ jobs:
|
||||
needs: prepare
|
||||
if: needs.prepare.outputs.target != 'skip'
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable
|
||||
pnpm install --frozen-lockfile
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- run: pnpm lint
|
||||
- run: pnpm build
|
||||
- name: 🧪 Lint
|
||||
shell: bash
|
||||
run: pnpm lint
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: 🏗️ Build Test
|
||||
shell: bash
|
||||
run: pnpm build
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NEXT_PUBLIC_BASE_URL: https://dummy.test
|
||||
@@ -94,70 +177,132 @@ jobs:
|
||||
needs: prepare
|
||||
if: needs.prepare.outputs.target != 'skip'
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🐳 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: 🔐 Registry Login
|
||||
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
|
||||
run: echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
|
||||
- name: 🏗️ Build and Push
|
||||
shell: bash
|
||||
run: |
|
||||
docker build \
|
||||
docker buildx build \
|
||||
--pull \
|
||||
--platform linux/arm64 \
|
||||
--build-arg NPM_TOKEN=${{ secrets.NPM_TOKEN }} \
|
||||
--build-arg NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_base_url }} \
|
||||
--build-arg NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }} \
|
||||
--build-arg DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }} \
|
||||
-t registry.infra.mintel.me/mintel/mb-grid-solutions:${{ needs.prepare.outputs.image_tag }} .
|
||||
docker push registry.infra.mintel.me/mintel/mb-grid-solutions:${{ needs.prepare.outputs.image_tag }}
|
||||
--build-arg UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }} \
|
||||
-t registry.infra.mintel.me/mintel/mb-grid-solutions:${{ needs.prepare.outputs.image_tag }} \
|
||||
--push .
|
||||
|
||||
deploy:
|
||||
name: 🚀 Deploy
|
||||
needs: [prepare, build, qa]
|
||||
if: needs.prepare.outputs.target != 'skip'
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: 🚀 Deploy via SSH
|
||||
uses: appleboy/ssh-action@master
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
host: ${{ secrets.SSH_HOST }}
|
||||
username: root
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
script: |
|
||||
fetch-depth: 1
|
||||
|
||||
- name: 🚀 Deploy via SSH
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Deploying to alpha.mintel.me"
|
||||
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
|
||||
|
||||
# Generate Environment File
|
||||
cat > .env.deploy << 'EOF'
|
||||
ENV_FILE=${{ needs.prepare.outputs.env_file }}
|
||||
IMAGE_TAG=${{ needs.prepare.outputs.image_tag }}
|
||||
TRAEFIK_HOST=${{ needs.prepare.outputs.traefik_host }}
|
||||
TRAEFIK_RULE=${{ needs.prepare.outputs.traefik_rule }}
|
||||
TRAEFIK_MIDDLEWARES=${{ needs.prepare.outputs.traefik_middlewares }}
|
||||
GATEKEEPER_RULE=${{ needs.prepare.outputs.gatekeeper_rule }}
|
||||
GATEKEEPER_HOST=${{ needs.prepare.outputs.gatekeeper_host }}
|
||||
PROJECT_NAME=${{ needs.prepare.outputs.project_name }}
|
||||
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_base_url }}
|
||||
|
||||
# Directus
|
||||
DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }}
|
||||
DIRECTUS_HOST=${{ needs.prepare.outputs.directus_host }}
|
||||
INTERNAL_DIRECTUS_URL=http://directus:8055
|
||||
DIRECTUS_API_TOKEN=${{ secrets.DIRECTUS_API_TOKEN || vars.DIRECTUS_API_TOKEN }}
|
||||
DIRECTUS_ADMIN_EMAIL=${{ secrets.DIRECTUS_ADMIN_EMAIL || vars.DIRECTUS_ADMIN_EMAIL || 'admin@mintel.me' }}
|
||||
DIRECTUS_ADMIN_PASSWORD=${{ secrets.DIRECTUS_ADMIN_PASSWORD || vars.DIRECTUS_ADMIN_PASSWORD }}
|
||||
DIRECTUS_DB_NAME=${{ secrets.DIRECTUS_DB_NAME || vars.DIRECTUS_DB_NAME || 'directus' }}
|
||||
DIRECTUS_DB_USER=${{ secrets.DIRECTUS_DB_USER || vars.DIRECTUS_DB_USER || 'directus' }}
|
||||
DIRECTUS_DB_PASSWORD=${{ secrets.DIRECTUS_DB_PASSWORD || vars.DIRECTUS_DB_PASSWORD }}
|
||||
DIRECTUS_KEY=${{ secrets.DIRECTUS_KEY || vars.DIRECTUS_KEY }}
|
||||
DIRECTUS_SECRET=${{ secrets.DIRECTUS_SECRET || vars.DIRECTUS_SECRET }}
|
||||
|
||||
# SMTP Config
|
||||
SMTP_HOST=${{ secrets.SMTP_HOST || vars.SMTP_HOST }}
|
||||
SMTP_PORT=${{ secrets.SMTP_PORT || vars.SMTP_PORT || '587' }}
|
||||
SMTP_SECURE=${{ secrets.SMTP_SECURE || vars.SMTP_SECURE || 'false' }}
|
||||
SMTP_USER=${{ secrets.SMTP_USER || vars.SMTP_USER }}
|
||||
SMTP_PASS=${{ secrets.SMTP_PASS || vars.SMTP_PASS }}
|
||||
SMTP_FROM=${{ secrets.SMTP_FROM || vars.SMTP_FROM }}
|
||||
CONTACT_RECIPIENT=${{ secrets.CONTACT_RECIPIENT || vars.CONTACT_RECIPIENT }}
|
||||
|
||||
# Authentication
|
||||
GATEKEEPER_PASSWORD=${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD }}
|
||||
AUTH_COOKIE_NAME=${{ secrets.AUTH_COOKIE_NAME || vars.AUTH_COOKIE_NAME || 'mintel_gatekeeper_session' }}
|
||||
COOKIE_DOMAIN=${{ secrets.COOKIE_DOMAIN || vars.COOKIE_DOMAIN || '.mb-grid-solutions.com' }}
|
||||
|
||||
# External Services
|
||||
SENTRY_DSN=${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }}
|
||||
GOTIFY_URL=${{ secrets.GOTIFY_URL || vars.GOTIFY_URL }}
|
||||
GOTIFY_TOKEN=${{ secrets.GOTIFY_TOKEN || vars.GOTIFY_TOKEN }}
|
||||
UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
|
||||
UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||
|
||||
# Project
|
||||
PROJECT_COLOR=${{ secrets.PROJECT_COLOR || vars.PROJECT_COLOR || '#82ed20' }}
|
||||
EOF
|
||||
|
||||
APP_DIR="/home/deploy/sites/mb-grid-solutions.com"
|
||||
ssh -o StrictHostKeyChecking=accept-new root@alpha.mintel.me "mkdir -p $APP_DIR"
|
||||
|
||||
scp -o StrictHostKeyChecking=accept-new .env.deploy root@alpha.mintel.me:$APP_DIR/${{ needs.prepare.outputs.env_file }}
|
||||
scp -o StrictHostKeyChecking=accept-new docker-compose.yaml root@alpha.mintel.me:$APP_DIR/docker-compose.yaml
|
||||
|
||||
ssh -o StrictHostKeyChecking=accept-new root@alpha.mintel.me bash << 'EOF'
|
||||
set -e
|
||||
APP_DIR="/home/deploy/sites/mb-grid-solutions.com"
|
||||
mkdir -p $APP_DIR
|
||||
cd $APP_DIR
|
||||
|
||||
# Update Environment
|
||||
cat > ${{ needs.prepare.outputs.env_file }} << EOF
|
||||
IMAGE_TAG=${{ needs.prepare.outputs.image_tag }}
|
||||
TRAEFIK_HOST=${{ needs.prepare.outputs.traefik_host }}
|
||||
PROJECT_NAME=${{ needs.prepare.outputs.project_name }}
|
||||
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_base_url }}
|
||||
|
||||
# Directus
|
||||
DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }}
|
||||
DIRECTUS_HOST=${{ needs.prepare.outputs.directus_host }}
|
||||
DIRECTUS_API_TOKEN=${{ secrets.DIRECTUS_API_TOKEN }}
|
||||
DIRECTUS_ADMIN_EMAIL=${{ secrets.DIRECTUS_ADMIN_EMAIL || 'admin@mintel.me' }}
|
||||
DIRECTUS_ADMIN_PASSWORD=${{ secrets.DIRECTUS_ADMIN_PASSWORD }}
|
||||
DIRECTUS_DB_NAME=${{ secrets.DIRECTUS_DB_NAME || 'directus' }}
|
||||
DIRECTUS_DB_USER=${{ secrets.DIRECTUS_DB_USER || 'directus' }}
|
||||
DIRECTUS_DB_PASSWORD=${{ secrets.DIRECTUS_DB_PASSWORD }}
|
||||
DIRECTUS_KEY=${{ secrets.DIRECTUS_KEY }}
|
||||
DIRECTUS_SECRET=${{ secrets.DIRECTUS_SECRET }}
|
||||
EOF
|
||||
|
||||
# Sync docker-compose
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
|
||||
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
docker compose -p "${{ needs.prepare.outputs.project_name }}" --env-file ${{ needs.prepare.outputs.env_file }} pull
|
||||
docker compose -p "${{ needs.prepare.outputs.project_name }}" --env-file ${{ needs.prepare.outputs.env_file }} up -d --remove-orphans
|
||||
docker system prune -f --filter "until=24h"
|
||||
EOF
|
||||
|
||||
notifications:
|
||||
name: 🔔 Notifications
|
||||
needs: [prepare, deploy]
|
||||
if: always()
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: Notify Gotify
|
||||
shell: bash
|
||||
run: |
|
||||
STATUS="${{ needs.deploy.result }}"
|
||||
COLOR="info"
|
||||
|
||||
21
Dockerfile
21
Dockerfile
@@ -3,18 +3,31 @@ FROM registry.infra.mintel.me/mintel/nextjs:latest AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Ensure we are in a clean, standalone environment
|
||||
RUN rm -rf packages apps pnpm-workspace.yaml 2>/dev/null || true
|
||||
|
||||
# Build-time environment variables for Next.js
|
||||
ARG NEXT_PUBLIC_BASE_URL
|
||||
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||
ARG NEXT_PUBLIC_UMAMI_SCRIPT_URL
|
||||
ARG UMAMI_API_ENDPOINT
|
||||
ARG NEXT_PUBLIC_TARGET
|
||||
ARG DIRECTUS_URL
|
||||
ARG NPM_TOKEN
|
||||
|
||||
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
|
||||
ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||
ENV NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL
|
||||
ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
||||
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
|
||||
ENV DIRECTUS_URL=$DIRECTUS_URL
|
||||
ENV NPM_TOKEN=$NPM_TOKEN
|
||||
ENV SENTRY_SUPPRESS_TURBOPACK_WARNING=1
|
||||
|
||||
# Enable corepack
|
||||
RUN corepack enable
|
||||
|
||||
# Copy package files
|
||||
COPY package.json pnpm-lock.yaml* .npmrc ./
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --no-frozen-lockfile
|
||||
|
||||
# Copy local files
|
||||
COPY . .
|
||||
|
||||
@@ -5,6 +5,7 @@ import "../globals.css";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages } from "next-intl/server";
|
||||
import { notFound } from "next/navigation";
|
||||
import { LazyMotion, domAnimation } from "framer-motion";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
@@ -70,9 +71,9 @@ export default async function RootLayout({
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ locale: string }>;
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
const { locale } = params;
|
||||
|
||||
// Validate that the incoming `locale` is supported
|
||||
if (locale !== "de") {
|
||||
@@ -105,6 +106,13 @@ export default async function RootLayout({
|
||||
},
|
||||
};
|
||||
|
||||
// Track pageview on the server
|
||||
// This is safe to call here because layout is a Server Component
|
||||
const services = (
|
||||
await import("@/lib/services/create-services.server")
|
||||
).getServerAppServices();
|
||||
services.analytics.trackPageview();
|
||||
|
||||
return (
|
||||
<html lang={locale} className={`${inter.variable}`}>
|
||||
<head>
|
||||
@@ -115,7 +123,9 @@ export default async function RootLayout({
|
||||
</head>
|
||||
<body className="antialiased">
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<Layout>{children}</Layout>
|
||||
<LazyMotion features={domAnimation}>
|
||||
<Layout>{children}</Layout>
|
||||
</LazyMotion>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -143,25 +143,27 @@ export default function About() {
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{t.raw("manifest.items").map((item: any, i: number) => {
|
||||
const Icon = manifestIcons[i];
|
||||
return (
|
||||
<Reveal key={i} delay={i * 0.1}>
|
||||
<div className="bg-white/5 p-10 rounded-3xl border border-white/10 group hover:-translate-y-1 transition-[box-shadow,transform] duration-300 h-full motion-fix relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-accent/0 group-hover:bg-accent/50 transition-all duration-500" />
|
||||
<div className="text-accent mb-6">
|
||||
<Icon size={32} />
|
||||
{t
|
||||
.raw("manifest.items")
|
||||
.map((item: { title: string; desc: string }, i: number) => {
|
||||
const Icon = manifestIcons[i];
|
||||
return (
|
||||
<Reveal key={i} delay={i * 0.1}>
|
||||
<div className="bg-white/5 p-10 rounded-3xl border border-white/10 group hover:-translate-y-1 transition-[box-shadow,transform] duration-300 h-full motion-fix relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-accent/0 group-hover:bg-accent/50 transition-all duration-500" />
|
||||
<div className="text-accent mb-6">
|
||||
<Icon size={32} />
|
||||
</div>
|
||||
<h4 className="text-xl font-bold text-white mb-4">
|
||||
{i + 1}. {item.title}
|
||||
</h4>
|
||||
<p className="text-slate-400 leading-relaxed">
|
||||
{item.desc}
|
||||
</p>
|
||||
</div>
|
||||
<h4 className="text-xl font-bold text-white mb-4">
|
||||
{i + 1}. {item.title}
|
||||
</h4>
|
||||
<p className="text-slate-400 leading-relaxed">
|
||||
{item.desc}
|
||||
</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
);
|
||||
})}
|
||||
</Reveal>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { m, LazyMotion, domAnimation } from "framer-motion";
|
||||
import React, { useState } from "react";
|
||||
import { m } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
|
||||
@@ -62,15 +62,13 @@ export const Button = ({
|
||||
);
|
||||
|
||||
const spotlight = (
|
||||
<LazyMotion features={domAnimation}>
|
||||
<m.div
|
||||
className="absolute inset-0 z-0 pointer-events-none transition-opacity duration-500"
|
||||
style={{
|
||||
opacity: isHovered ? 1 : 0,
|
||||
background: `radial-gradient(600px circle at ${mousePosition.x}px ${mousePosition.y}px, rgba(255,255,255,0.15), transparent 40%)`,
|
||||
}}
|
||||
/>
|
||||
</LazyMotion>
|
||||
<m.div
|
||||
className="absolute inset-0 z-0 pointer-events-none transition-opacity duration-500"
|
||||
style={{
|
||||
opacity: isHovered ? 1 : 0,
|
||||
background: `radial-gradient(600px circle at ${mousePosition.x}px ${mousePosition.y}px, rgba(255,255,255,0.15), transparent 40%)`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const buttonProps = {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Mail, MapPin, CheckCircle } from "lucide-react";
|
||||
import { Button } from "./Button";
|
||||
import { Counter } from "./Counter";
|
||||
@@ -57,7 +58,7 @@ export default function Contact() {
|
||||
buttonText: t("form.tryAgain") || "Erneut versuchen",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setStatusModal({
|
||||
isOpen: true,
|
||||
type: "error",
|
||||
@@ -279,12 +280,12 @@ export default function Contact() {
|
||||
<p className="text-xs text-slate-400 text-center">
|
||||
{t.rich("form.privacyNote", {
|
||||
link: (chunks) => (
|
||||
<a
|
||||
<Link
|
||||
href="/datenschutz"
|
||||
className="text-accent hover:underline font-semibold"
|
||||
>
|
||||
{chunks}
|
||||
</a>
|
||||
</Link>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { m, LazyMotion, domAnimation } from "framer-motion";
|
||||
import { m } from "framer-motion";
|
||||
import {
|
||||
BarChart3,
|
||||
CheckCircle2,
|
||||
@@ -326,55 +326,53 @@ export default function Home() {
|
||||
<div className="tech-corner bottom-8 right-8 border-b-2 border-r-2" />
|
||||
|
||||
<div className="absolute top-0 right-0 w-1/2 h-full opacity-10 pointer-events-none">
|
||||
<LazyMotion features={domAnimation}>
|
||||
<svg
|
||||
viewBox="0 0 400 400"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<m.circle
|
||||
animate={{ r: [400, 410, 400], opacity: [0.1, 0.2, 0.1] }}
|
||||
transition={{
|
||||
duration: 5,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
cx="400"
|
||||
cy="0"
|
||||
r="400"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<m.circle
|
||||
animate={{ r: [300, 310, 300], opacity: [0.1, 0.2, 0.1] }}
|
||||
transition={{
|
||||
duration: 4,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay: 0.5,
|
||||
}}
|
||||
cx="400"
|
||||
cy="0"
|
||||
r="300"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<m.circle
|
||||
animate={{ r: [200, 210, 200], opacity: [0.1, 0.2, 0.1] }}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay: 1,
|
||||
}}
|
||||
cx="400"
|
||||
cy="0"
|
||||
r="200"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
</LazyMotion>
|
||||
<svg
|
||||
viewBox="0 0 400 400"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<m.circle
|
||||
animate={{ r: [400, 410, 400], opacity: [0.1, 0.2, 0.1] }}
|
||||
transition={{
|
||||
duration: 5,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
cx="400"
|
||||
cy="0"
|
||||
r="400"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<m.circle
|
||||
animate={{ r: [300, 310, 300], opacity: [0.1, 0.2, 0.1] }}
|
||||
transition={{
|
||||
duration: 4,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay: 0.5,
|
||||
}}
|
||||
cx="400"
|
||||
cy="0"
|
||||
r="300"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<m.circle
|
||||
animate={{ r: [200, 210, 200], opacity: [0.1, 0.2, 0.1] }}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay: 1,
|
||||
}}
|
||||
cx="400"
|
||||
cy="0"
|
||||
r="200"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10">
|
||||
|
||||
@@ -52,10 +52,10 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
className="fixed top-0 left-0 right-0 z-[100]"
|
||||
>
|
||||
<header
|
||||
className={`transition-all duration-300 flex items-center py-1 ${
|
||||
className={`transition-all duration-300 flex items-center ${
|
||||
isScrolled
|
||||
? "bg-white/90 backdrop-blur-lg border-b border-slate-200 shadow-sm"
|
||||
: "bg-gradient-to-b from-white/80 via-white/40 to-transparent"
|
||||
? "bg-white/90 backdrop-blur-lg border-b border-slate-200 shadow-sm py-2"
|
||||
: "bg-gradient-to-b from-white/80 via-white/40 to-transparent py-4"
|
||||
}`}
|
||||
>
|
||||
<div className="container-custom flex justify-between items-center w-full relative z-10">
|
||||
@@ -65,7 +65,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
aria-label={`${t("nav.home")} - Zur Startseite`}
|
||||
>
|
||||
<div
|
||||
className={`relative transition-all duration-300 ${isScrolled ? "h-[50px] md:h-[80px] w-[120px] md:w-[200px] mt-0 mb-[-10px]" : "h-[80px] md:h-[140px] w-[180px] md:w-[320px] mt-2 md:mt-4 mb-[-20px] md:mb-[-40px]"}`}
|
||||
className={`relative transition-all duration-300 ${isScrolled ? "h-[50px] md:h-[60px] w-[120px] md:w-[150px]" : "h-[70px] md:h-[100px] w-[160px] md:w-[240px]"}`}
|
||||
>
|
||||
<Image
|
||||
src="/assets/logo.png"
|
||||
@@ -116,38 +116,36 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
</Reveal>
|
||||
|
||||
{/* Mobile Menu Overlay */}
|
||||
<LazyMotion features={domAnimation}>
|
||||
<AnimatePresence>
|
||||
{isMobileMenuOpen && (
|
||||
<m.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className="fixed inset-0 z-[90] bg-white pt-32 px-6 md:hidden"
|
||||
>
|
||||
<nav className="flex flex-col gap-4">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={`flex items-center gap-4 p-4 rounded-xl text-lg font-semibold transition-all ${
|
||||
isActive(link.href)
|
||||
? "text-accent bg-accent/5"
|
||||
: "text-slate-600 hover:text-primary hover:bg-slate-50"
|
||||
}`}
|
||||
>
|
||||
<link.icon size={24} />
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
<Button href="/kontakt" className="mt-4 w-full">
|
||||
{t("nav.cta")}
|
||||
</Button>
|
||||
</nav>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</LazyMotion>
|
||||
<AnimatePresence>
|
||||
{isMobileMenuOpen && (
|
||||
<m.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className="fixed inset-0 z-[90] bg-white pt-32 px-6 md:hidden"
|
||||
>
|
||||
<nav className="flex flex-col gap-4">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={`flex items-center gap-4 p-4 rounded-xl text-lg font-semibold transition-all ${
|
||||
isActive(link.href)
|
||||
? "text-accent bg-accent/5"
|
||||
: "text-slate-600 hover:text-primary hover:bg-slate-50"
|
||||
}`}
|
||||
>
|
||||
<link.icon size={24} />
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
<Button href="/kontakt" className="mt-4 w-full">
|
||||
{t("nav.cta")}
|
||||
</Button>
|
||||
</nav>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<main className="flex-grow">{children}</main>
|
||||
|
||||
@@ -168,18 +166,16 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
<div className="absolute inset-0 grid-pattern opacity-[0.08] pointer-events-none" />
|
||||
|
||||
{/* Animated Tech Lines */}
|
||||
<LazyMotion features={domAnimation}>
|
||||
<m.div
|
||||
animate={{ x: ["-100%", "100%"] }}
|
||||
transition={{ duration: 15, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-accent/30 to-transparent"
|
||||
/>
|
||||
<m.div
|
||||
animate={{ x: ["100%", "-100%"] }}
|
||||
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-accent/20 to-transparent"
|
||||
/>
|
||||
</LazyMotion>
|
||||
<m.div
|
||||
animate={{ x: ["-100%", "100%"] }}
|
||||
transition={{ duration: 15, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-accent/30 to-transparent"
|
||||
/>
|
||||
<m.div
|
||||
animate={{ x: ["100%", "-100%"] }}
|
||||
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-accent/20 to-transparent"
|
||||
/>
|
||||
|
||||
{/* Corner Accents */}
|
||||
<div className="tech-corner top-8 left-8 border-t border-l border-white/10 group-hover:border-accent/30 transition-colors duration-700" />
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { m, LazyMotion, domAnimation } from 'framer-motion';
|
||||
import React from "react";
|
||||
import { m } from "framer-motion";
|
||||
|
||||
interface RevealProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
delay?: number;
|
||||
direction?: 'up' | 'down' | 'left' | 'right';
|
||||
direction?: "up" | "down" | "left" | "right";
|
||||
fullWidth?: boolean;
|
||||
viewportMargin?: string;
|
||||
trigger?: 'inView' | 'mount';
|
||||
trigger?: "inView" | "mount";
|
||||
}
|
||||
|
||||
export const Reveal = ({
|
||||
children,
|
||||
className = '',
|
||||
className = "",
|
||||
delay = 0,
|
||||
direction = 'up',
|
||||
direction = "up",
|
||||
fullWidth = false,
|
||||
viewportMargin = "-50px",
|
||||
trigger = 'inView'
|
||||
trigger = "inView",
|
||||
}: RevealProps) => {
|
||||
const directions = {
|
||||
up: { y: 30 },
|
||||
@@ -30,35 +30,45 @@ export const Reveal = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<LazyMotion features={domAnimation}>
|
||||
<m.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
...directions[direction]
|
||||
...directions[direction],
|
||||
}}
|
||||
animate={trigger === 'mount' ? {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
y: 0
|
||||
} : undefined}
|
||||
whileInView={trigger === 'inView' ? {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
y: 0
|
||||
} : undefined}
|
||||
viewport={trigger === 'inView' ? { once: true, margin: viewportMargin } : undefined}
|
||||
animate={
|
||||
trigger === "mount"
|
||||
? {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
whileInView={
|
||||
trigger === "inView"
|
||||
? {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
viewport={
|
||||
trigger === "inView"
|
||||
? { once: true, margin: viewportMargin }
|
||||
: undefined
|
||||
}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 50,
|
||||
damping: 20,
|
||||
mass: 1,
|
||||
delay: delay
|
||||
delay: delay,
|
||||
}}
|
||||
className={`${fullWidth ? 'w-full' : ''} ${className} motion-fix`}
|
||||
className={`${fullWidth ? "w-full" : ""} ${className} motion-fix will-change-[transform,opacity]`}
|
||||
>
|
||||
{children}
|
||||
</m.div>
|
||||
</LazyMotion>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -68,13 +78,12 @@ interface StaggerProps {
|
||||
staggerDelay?: number;
|
||||
}
|
||||
|
||||
export const Stagger = ({
|
||||
children,
|
||||
className = '',
|
||||
staggerDelay = 0.1
|
||||
export const Stagger = ({
|
||||
children,
|
||||
className = "",
|
||||
staggerDelay = 0.1,
|
||||
}: StaggerProps) => {
|
||||
return (
|
||||
<LazyMotion features={domAnimation}>
|
||||
<m.div
|
||||
initial="initial"
|
||||
whileInView="animate"
|
||||
@@ -90,6 +99,5 @@ export const Stagger = ({
|
||||
>
|
||||
{children}
|
||||
</m.div>
|
||||
</LazyMotion>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { m, LazyMotion, domAnimation } from 'framer-motion';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { m } from "framer-motion";
|
||||
|
||||
export const TileGrid = () => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
@@ -12,42 +12,41 @@ export const TileGrid = () => {
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const rows = 15;
|
||||
const cols = 20;
|
||||
const rows = 7;
|
||||
const cols = 8;
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 pointer-events-none overflow-hidden z-[1]">
|
||||
<LazyMotion features={domAnimation}>
|
||||
<div className="flex flex-col gap-3 min-w-[120%] min-h-[120%] -left-[10%] -top-[10%] absolute">
|
||||
<div className="flex flex-col gap-8 md:gap-12 min-w-[120%] min-h-[120%] -left-[10%] -top-[10%] absolute">
|
||||
{[...Array(rows)].map((_, rowIndex) => (
|
||||
<div
|
||||
key={rowIndex}
|
||||
className="flex gap-3 justify-center"
|
||||
className="flex gap-8 md:gap-12 justify-center"
|
||||
style={{
|
||||
transform: rowIndex % 2 === 0 ? 'translateX(0)' : 'translateX(80px)',
|
||||
transform:
|
||||
rowIndex % 2 === 0 ? "translateX(0)" : "translateX(100px)",
|
||||
}}
|
||||
>
|
||||
{[...Array(cols)].map((_, colIndex) => (
|
||||
<m.div
|
||||
key={`${rowIndex}-${colIndex}`}
|
||||
initial={{ opacity: 0.05 }}
|
||||
initial={{ opacity: 0.03 }}
|
||||
animate={{
|
||||
opacity: [0.05, Math.random() > 0.9 ? 0.25 : 0.05, 0.05],
|
||||
scale: [1, Math.random() > 0.9 ? 1.05 : 1, 1]
|
||||
opacity: [0.03, Math.random() > 0.8 ? 0.15 : 0.03, 0.03],
|
||||
scale: [1, Math.random() > 0.8 ? 1.02 : 1, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: 5 + Math.random() * 5,
|
||||
duration: 8 + Math.random() * 8,
|
||||
repeat: Infinity,
|
||||
delay: Math.random() * 20,
|
||||
ease: "easeInOut"
|
||||
delay: Math.random() * 15,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="w-24 h-24 md:w-40 md:h-40 bg-white/10 backdrop-blur-[2px] rounded-2xl md:rounded-3xl border border-white/20 shadow-[0_8px_32px_0_rgba(31,38,135,0.07)] shrink-0"
|
||||
className="w-32 h-32 md:w-56 md:h-56 bg-white/5 rounded-3xl md:rounded-[3rem] border border-white/10 shadow-[0_8px_32px_0_rgba(31,38,135,0.03)] shrink-0 will-change-transform"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</LazyMotion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
48
components/analytics/AnalyticsProvider.tsx
Normal file
48
components/analytics/AnalyticsProvider.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { getAppServices } from "@/lib/services/create-services";
|
||||
|
||||
/**
|
||||
* AnalyticsProvider Component
|
||||
*
|
||||
* Automatically tracks pageviews on client-side route changes.
|
||||
* This component should be placed inside your layout to handle navigation events.
|
||||
*
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} [props.websiteId] - The Umami website ID (passed from server config)
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // In your layout.tsx
|
||||
* const { websiteId } = config.analytics.umami;
|
||||
* <AnalyticsProvider websiteId={websiteId} />
|
||||
* ```
|
||||
*/
|
||||
export default function AnalyticsProvider({
|
||||
websiteId,
|
||||
}: {
|
||||
websiteId?: string;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (!pathname) return;
|
||||
|
||||
const services = getAppServices();
|
||||
const url = `${pathname}${searchParams?.size ? `?${searchParams.toString()}` : ""}`;
|
||||
|
||||
// Track pageview with the full URL
|
||||
services.analytics.trackPageview(url);
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log("[Umami] Tracked pageview:", url);
|
||||
}
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
if (!websiteId) return null;
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -8,39 +8,49 @@ services:
|
||||
- ${ENV_FILE:-.env}
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}.rule=Host(`${TRAEFIK_HOST:-mb-grid-solutions.localhost}`)"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}.entrypoints=websecure"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}.tls.certresolver=le"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}.tls=true"
|
||||
- "traefik.http.services.${PROJECT_NAME:-mb-grid-solutions}.loadbalancer.server.port=3000"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}.middlewares=${PROJECT_NAME:-mb-grid-solutions}-auth"
|
||||
- "traefik.http.routers.${PROJECT_NAME}.rule=${TRAEFIK_RULE:-Host(`${TRAEFIK_HOST:-mb-grid-solutions.localhost}`)}"
|
||||
- "traefik.http.routers.${PROJECT_NAME}.entrypoints=websecure"
|
||||
- "traefik.http.routers.${PROJECT_NAME}.tls.certresolver=le"
|
||||
- "traefik.http.routers.${PROJECT_NAME}.tls=true"
|
||||
- "traefik.http.services.${PROJECT_NAME}.loadbalancer.server.port=3000"
|
||||
- "traefik.http.routers.${PROJECT_NAME}.middlewares=${TRAEFIK_MIDDLEWARES:-${PROJECT_NAME}-auth}"
|
||||
- "traefik.docker.network=infra"
|
||||
|
||||
# Gatekeeper Router
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}-gatekeeper.rule=Host(`${TRAEFIK_HOST:-mb-grid-solutions.localhost}`) && PathPrefix(`/gatekeeper`)"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}-gatekeeper.entrypoints=websecure"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}-gatekeeper.tls.certresolver=le"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}-gatekeeper.tls=true"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}-gatekeeper.service=${PROJECT_NAME:-mb-grid-solutions}-gatekeeper"
|
||||
# Gatekeeper Router (Shared Host + dedicated Subdomain)
|
||||
- "traefik.http.routers.${PROJECT_NAME}-gatekeeper.rule=${GATEKEEPER_RULE:-(Host(`${TRAEFIK_HOST:-mb-grid-solutions.localhost}`) && PathPrefix(`/gatekeeper`)) || Host(`gatekeeper.${TRAEFIK_HOST:-mb-grid-solutions.localhost}`)}"
|
||||
- "traefik.http.routers.${PROJECT_NAME}-gatekeeper.entrypoints=websecure"
|
||||
- "traefik.http.routers.${PROJECT_NAME}-gatekeeper.tls.certresolver=le"
|
||||
- "traefik.http.routers.${PROJECT_NAME}-gatekeeper.tls=true"
|
||||
- "traefik.http.routers.${PROJECT_NAME}-gatekeeper.service=${PROJECT_NAME}-gatekeeper"
|
||||
|
||||
# Auth Middleware Definition
|
||||
- "traefik.http.middlewares.${PROJECT_NAME:-mb-grid-solutions}-auth.forwardauth.address=http://gatekeeper:3000/api/verify"
|
||||
- "traefik.http.middlewares.${PROJECT_NAME:-mb-grid-solutions}-auth.forwardauth.trustForwardHeader=true"
|
||||
- "traefik.http.middlewares.${PROJECT_NAME:-mb-grid-solutions}-auth.forwardauth.authResponseHeaders=X-Auth-User"
|
||||
- "traefik.http.middlewares.${PROJECT_NAME}-auth.forwardauth.address=http://${PROJECT_NAME}-gatekeeper:3000/api/verify"
|
||||
- "traefik.http.middlewares.${PROJECT_NAME}-auth.forwardauth.trustForwardHeader=true"
|
||||
- "traefik.http.middlewares.${PROJECT_NAME}-auth.forwardauth.authResponseHeaders=X-Auth-User"
|
||||
- "traefik.docker.network=infra"
|
||||
|
||||
gatekeeper:
|
||||
image: registry.infra.mintel.me/mintel/gatekeeper:latest
|
||||
container_name: ${PROJECT_NAME:-mb-grid-solutions}-gatekeeper
|
||||
restart: always
|
||||
networks:
|
||||
- infra
|
||||
infra:
|
||||
aliases:
|
||||
- ${PROJECT_NAME:-mb-grid-solutions}-gatekeeper
|
||||
env_file:
|
||||
- ${ENV_FILE:-.env}
|
||||
environment:
|
||||
PORT: 3000
|
||||
PROJECT_NAME: "MB Grid Solutions"
|
||||
PROJECT_COLOR: "#82ed20"
|
||||
PORT: ${PORT:-3000}
|
||||
PROJECT_NAME: ${PROJECT_NAME:-MB Grid Solutions}
|
||||
PROJECT_COLOR: ${PROJECT_COLOR:-#82ed20}
|
||||
COOKIE_DOMAIN: ${COOKIE_DOMAIN:-.mb-grid-solutions.com}
|
||||
AUTH_COOKIE_NAME: ${AUTH_COOKIE_NAME:-mintel_gatekeeper_session}
|
||||
GATEKEEPER_PASSWORD: ${GATEKEEPER_PASSWORD:-mintel}
|
||||
# Dedicated Base URL for Gatekeeper subdomain to prevent redirect loops
|
||||
NEXT_PUBLIC_BASE_URL: https://${GATEKEEPER_HOST:-gatekeeper.mb-grid-solutions.localhost}
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.services.${PROJECT_NAME:-mb-grid-solutions}-gatekeeper.loadbalancer.server.port=3000"
|
||||
- "traefik.http.services.${PROJECT_NAME}-gatekeeper.loadbalancer.server.port=3000"
|
||||
- "traefik.docker.network=infra"
|
||||
|
||||
directus:
|
||||
image: directus/directus:11
|
||||
@@ -63,17 +73,23 @@ services:
|
||||
DB_DATABASE: ${DIRECTUS_DB_NAME:-directus}
|
||||
DB_USER: ${DIRECTUS_DB_USER:-directus}
|
||||
DB_PASSWORD: ${DIRECTUS_DB_PASSWORD:-directus}
|
||||
# Telemetry & Performance
|
||||
LOGGER_LEVEL: ${LOG_LEVEL:-info}
|
||||
SENTRY_DSN: ${SENTRY_DSN}
|
||||
SENTRY_ENVIRONMENT: ${TARGET:-development}
|
||||
volumes:
|
||||
- ./directus/uploads:/directus/uploads
|
||||
- ./directus/extensions:/directus/extensions
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}-directus.rule=Host(`${DIRECTUS_HOST:-cms.mb-grid-solutions.localhost}`)"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}-directus.entrypoints=websecure"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}-directus.tls.certresolver=le"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}-directus.tls=true"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}-directus.middlewares=${PROJECT_NAME:-mb-grid-solutions}-auth"
|
||||
- "traefik.http.services.${PROJECT_NAME:-mb-grid-solutions}-directus.loadbalancer.server.port=8055"
|
||||
- "traefik.http.routers.${PROJECT_NAME}-directus.rule=Host(`${DIRECTUS_HOST:-cms.mb-grid-solutions.localhost}`)"
|
||||
- "traefik.http.routers.${PROJECT_NAME}-directus.entrypoints=websecure"
|
||||
- "traefik.http.routers.${PROJECT_NAME}-directus.tls.certresolver=le"
|
||||
- "traefik.http.routers.${PROJECT_NAME}-directus.tls=true"
|
||||
- "traefik.http.routers.${PROJECT_NAME}-directus.middlewares=${PROJECT_NAME}-forward,compress"
|
||||
- "traefik.http.services.${PROJECT_NAME}-directus.loadbalancer.server.port=8055"
|
||||
- "traefik.http.middlewares.${PROJECT_NAME}-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
|
||||
- "traefik.docker.network=infra"
|
||||
|
||||
directus-db:
|
||||
image: postgres:15-alpine
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { nextConfig } from "@mintel/eslint-config/next";
|
||||
|
||||
export default [
|
||||
...nextConfig,
|
||||
];
|
||||
@@ -27,11 +27,9 @@ function createConfig() {
|
||||
|
||||
analytics: {
|
||||
umami: {
|
||||
websiteId: env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
|
||||
scriptUrl: env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
|
||||
// The proxied path used in the frontend
|
||||
proxyPath: "/stats/script.js",
|
||||
enabled: Boolean(env.NEXT_PUBLIC_UMAMI_WEBSITE_ID),
|
||||
websiteId: env.UMAMI_WEBSITE_ID,
|
||||
apiEndpoint: env.UMAMI_API_ENDPOINT,
|
||||
enabled: Boolean(env.UMAMI_WEBSITE_ID),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -153,7 +151,7 @@ export function getMaskedConfig() {
|
||||
analytics: {
|
||||
umami: {
|
||||
websiteId: mask(c.analytics.umami.websiteId),
|
||||
scriptUrl: c.analytics.umami.scriptUrl,
|
||||
apiEndpoint: c.analytics.umami.apiEndpoint,
|
||||
enabled: c.analytics.umami.enabled,
|
||||
},
|
||||
},
|
||||
|
||||
161
lib/env.ts
161
lib/env.ts
@@ -8,78 +8,99 @@ const preprocessEmptyString = (val: unknown) => (val === "" ? undefined : val);
|
||||
/**
|
||||
* Environment variable schema.
|
||||
*/
|
||||
export const envSchema = z.object({
|
||||
NODE_ENV: z
|
||||
.enum(["development", "production", "test"])
|
||||
.default("development"),
|
||||
NEXT_PUBLIC_BASE_URL: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().url().optional(),
|
||||
),
|
||||
NEXT_PUBLIC_TARGET: z
|
||||
.enum(["development", "testing", "staging", "production"])
|
||||
.optional(),
|
||||
export const envSchema = z
|
||||
.object({
|
||||
NODE_ENV: z
|
||||
.enum(["development", "production", "test"])
|
||||
.default("development"),
|
||||
NEXT_PUBLIC_BASE_URL: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().url().optional(),
|
||||
),
|
||||
NEXT_PUBLIC_TARGET: z
|
||||
.enum(["development", "testing", "staging", "production"])
|
||||
.optional(),
|
||||
|
||||
// Analytics
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().optional(),
|
||||
),
|
||||
NEXT_PUBLIC_UMAMI_SCRIPT_URL: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().url().default("https://analytics.infra.mintel.me/script.js"),
|
||||
),
|
||||
// Analytics
|
||||
UMAMI_WEBSITE_ID: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().optional(),
|
||||
),
|
||||
UMAMI_API_ENDPOINT: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().url().default("https://analytics.infra.mintel.me"),
|
||||
),
|
||||
|
||||
// Error Tracking
|
||||
SENTRY_DSN: z.preprocess(preprocessEmptyString, z.string().optional()),
|
||||
// Error Tracking
|
||||
SENTRY_DSN: z.preprocess(preprocessEmptyString, z.string().optional()),
|
||||
|
||||
// Logging
|
||||
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
||||
// Logging
|
||||
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
||||
|
||||
// Mail
|
||||
MAIL_HOST: z.preprocess(preprocessEmptyString, z.string().optional()),
|
||||
MAIL_PORT: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.coerce.number().default(587),
|
||||
),
|
||||
MAIL_USERNAME: z.preprocess(preprocessEmptyString, z.string().optional()),
|
||||
MAIL_PASSWORD: z.preprocess(preprocessEmptyString, z.string().optional()),
|
||||
MAIL_FROM: z.preprocess(preprocessEmptyString, z.string().optional()),
|
||||
MAIL_RECIPIENTS: z.preprocess(
|
||||
(val) => (typeof val === "string" ? val.split(",").filter(Boolean) : val),
|
||||
z.array(z.string()).default([]),
|
||||
),
|
||||
// Mail
|
||||
MAIL_HOST: z.preprocess(preprocessEmptyString, z.string().optional()),
|
||||
MAIL_PORT: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.coerce.number().default(587),
|
||||
),
|
||||
MAIL_USERNAME: z.preprocess(preprocessEmptyString, z.string().optional()),
|
||||
MAIL_PASSWORD: z.preprocess(preprocessEmptyString, z.string().optional()),
|
||||
MAIL_FROM: z.preprocess(preprocessEmptyString, z.string().optional()),
|
||||
MAIL_RECIPIENTS: z.preprocess(
|
||||
(val) => (typeof val === "string" ? val.split(",").filter(Boolean) : val),
|
||||
z.array(z.string()).default([]),
|
||||
),
|
||||
|
||||
// Directus
|
||||
DIRECTUS_URL: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().url().default("http://localhost:8055"),
|
||||
),
|
||||
DIRECTUS_ADMIN_EMAIL: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().optional(),
|
||||
),
|
||||
DIRECTUS_ADMIN_PASSWORD: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().optional(),
|
||||
),
|
||||
DIRECTUS_API_TOKEN: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().optional(),
|
||||
),
|
||||
INTERNAL_DIRECTUS_URL: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().url().optional(),
|
||||
),
|
||||
// Directus
|
||||
DIRECTUS_URL: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().url().default("http://localhost:8055"),
|
||||
),
|
||||
DIRECTUS_ADMIN_EMAIL: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().optional(),
|
||||
),
|
||||
DIRECTUS_ADMIN_PASSWORD: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().optional(),
|
||||
),
|
||||
DIRECTUS_API_TOKEN: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().optional(),
|
||||
),
|
||||
INTERNAL_DIRECTUS_URL: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().url().optional(),
|
||||
),
|
||||
|
||||
// Deploy Target
|
||||
TARGET: z
|
||||
.enum(["development", "testing", "staging", "production"])
|
||||
.optional(),
|
||||
// Gotify
|
||||
GOTIFY_URL: z.preprocess(preprocessEmptyString, z.string().url().optional()),
|
||||
GOTIFY_TOKEN: z.preprocess(preprocessEmptyString, z.string().optional()),
|
||||
});
|
||||
// Deploy Target
|
||||
TARGET: z
|
||||
.enum(["development", "testing", "staging", "production"])
|
||||
.optional(),
|
||||
// Gotify
|
||||
GOTIFY_URL: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().url().optional(),
|
||||
),
|
||||
GOTIFY_TOKEN: z.preprocess(preprocessEmptyString, z.string().optional()),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
const target = data.NEXT_PUBLIC_TARGET || data.TARGET;
|
||||
const isDev = target === "development" || !target;
|
||||
const isBuildTimeValidation =
|
||||
process.env.SKIP_RUNTIME_ENV_VALIDATION === "true";
|
||||
const isServer = typeof window === "undefined";
|
||||
|
||||
// Only enforce server-only variables when running on the server.
|
||||
// In the browser, non-NEXT_PUBLIC_ variables are undefined and should not trigger validation errors.
|
||||
if (isServer && !isDev && !isBuildTimeValidation && !data.MAIL_HOST) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "MAIL_HOST is required in non-development environments",
|
||||
path: ["MAIL_HOST"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type Env = z.infer<typeof envSchema>;
|
||||
|
||||
@@ -92,8 +113,12 @@ export function getRawEnv() {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
|
||||
NEXT_PUBLIC_TARGET: process.env.NEXT_PUBLIC_TARGET,
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
|
||||
NEXT_PUBLIC_UMAMI_SCRIPT_URL: process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
|
||||
UMAMI_WEBSITE_ID:
|
||||
process.env.UMAMI_WEBSITE_ID || process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
|
||||
UMAMI_API_ENDPOINT:
|
||||
process.env.UMAMI_API_ENDPOINT ||
|
||||
process.env.UMAMI_SCRIPT_URL ||
|
||||
process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
|
||||
SENTRY_DSN: process.env.SENTRY_DSN,
|
||||
LOG_LEVEL: process.env.LOG_LEVEL,
|
||||
MAIL_HOST: process.env.MAIL_HOST,
|
||||
|
||||
445
lib/services/analytics/README.md
Normal file
445
lib/services/analytics/README.md
Normal file
@@ -0,0 +1,445 @@
|
||||
# Analytics Service Layer
|
||||
|
||||
This directory contains the service layer implementation for analytics tracking in the KLZ Cables application.
|
||||
|
||||
## Overview
|
||||
|
||||
The analytics service layer provides a clean abstraction over different analytics implementations (Umami, Google Analytics, etc.) while maintaining a consistent API.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
lib/services/analytics/
|
||||
├── analytics-service.ts # Interface definition
|
||||
├── umami-analytics-service.ts # Umami implementation
|
||||
├── noop-analytics-service.ts # No-op fallback implementation
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### 1. AnalyticsService Interface (`analytics-service.ts`)
|
||||
|
||||
Defines the contract for all analytics services:
|
||||
|
||||
```typescript
|
||||
export interface AnalyticsService {
|
||||
track(eventName: string, props?: AnalyticsEventProperties): void;
|
||||
trackPageview(url?: string): void;
|
||||
}
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- Type-safe event properties
|
||||
- Consistent API across implementations
|
||||
- Well-documented with JSDoc comments
|
||||
|
||||
### 2. UmamiAnalyticsService (`umami-analytics-service.ts`)
|
||||
|
||||
Implements the `AnalyticsService` interface for Umami analytics.
|
||||
|
||||
**Features:**
|
||||
|
||||
- Type-safe event tracking
|
||||
- Automatic pageview tracking
|
||||
- Browser environment detection
|
||||
- Graceful error handling
|
||||
- Comprehensive JSDoc documentation
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { UmamiAnalyticsService } from "@/lib/services/analytics/umami-analytics-service";
|
||||
|
||||
const service = new UmamiAnalyticsService({ enabled: true });
|
||||
service.track("button_click", { button_id: "cta" });
|
||||
service.trackPageview("/products/123");
|
||||
```
|
||||
|
||||
### 3. NoopAnalyticsService (`noop-analytics-service.ts`)
|
||||
|
||||
A no-op implementation used as a fallback when analytics are disabled.
|
||||
|
||||
**Features:**
|
||||
|
||||
- Maintains the same API as other services
|
||||
- Safe to call even when analytics are disabled
|
||||
- No performance impact
|
||||
- Comprehensive JSDoc documentation
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { NoopAnalyticsService } from "@/lib/services/analytics/noop-analytics-service";
|
||||
|
||||
const service = new NoopAnalyticsService();
|
||||
service.track("button_click", { button_id: "cta" }); // Does nothing
|
||||
service.trackPageview("/products/123"); // Does nothing
|
||||
```
|
||||
|
||||
## Service Selection
|
||||
|
||||
The service layer automatically selects the appropriate implementation based on environment variables:
|
||||
|
||||
```typescript
|
||||
// In lib/services/create-services.ts
|
||||
const umamiEnabled = Boolean(process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID);
|
||||
|
||||
const analytics = umamiEnabled
|
||||
? new UmamiAnalyticsService({ enabled: true })
|
||||
: new NoopAnalyticsService();
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Required for Umami
|
||||
|
||||
```bash
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3
|
||||
```
|
||||
|
||||
### Optional (defaults provided)
|
||||
|
||||
```bash
|
||||
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### AnalyticsService Interface
|
||||
|
||||
#### `track(eventName: string, props?: AnalyticsEventProperties): void`
|
||||
|
||||
Track a custom event with optional properties.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `eventName` - The name of the event to track
|
||||
- `props` - Optional event properties (metadata)
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
service.track("product_add_to_cart", {
|
||||
product_id: "123",
|
||||
product_name: "Cable",
|
||||
price: 99.99,
|
||||
quantity: 1,
|
||||
});
|
||||
```
|
||||
|
||||
#### `trackPageview(url?: string): void`
|
||||
|
||||
Track a pageview.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `url` - The URL to track (defaults to current location)
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// Track current page
|
||||
service.trackPageview();
|
||||
|
||||
// Track custom URL
|
||||
service.trackPageview("/products/123?category=cables");
|
||||
```
|
||||
|
||||
### UmamiAnalyticsService
|
||||
|
||||
#### Constructor
|
||||
|
||||
```typescript
|
||||
new UmamiAnalyticsService(options: UmamiAnalyticsServiceOptions)
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
- `enabled: boolean` - Whether analytics are enabled
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
const service = new UmamiAnalyticsService({ enabled: true });
|
||||
```
|
||||
|
||||
### NoopAnalyticsService
|
||||
|
||||
#### Constructor
|
||||
|
||||
```typescript
|
||||
new NoopAnalyticsService();
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
const service = new NoopAnalyticsService();
|
||||
```
|
||||
|
||||
## Type Definitions
|
||||
|
||||
### AnalyticsEventProperties
|
||||
|
||||
```typescript
|
||||
type AnalyticsEventProperties = Record<
|
||||
string,
|
||||
string | number | boolean | null | undefined
|
||||
>;
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
const properties: AnalyticsEventProperties = {
|
||||
product_id: "123",
|
||||
product_name: "Cable",
|
||||
price: 99.99,
|
||||
quantity: 1,
|
||||
in_stock: true,
|
||||
discount: null,
|
||||
};
|
||||
```
|
||||
|
||||
### UmamiAnalyticsServiceOptions
|
||||
|
||||
```typescript
|
||||
type UmamiAnalyticsServiceOptions = {
|
||||
enabled: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use the Service Layer
|
||||
|
||||
Always use the service layer instead of calling Umami directly:
|
||||
|
||||
```typescript
|
||||
// ✅ Good
|
||||
import { getAppServices } from "@/lib/services/create-services";
|
||||
|
||||
const services = getAppServices();
|
||||
services.analytics.track("button_click", { button_id: "cta" });
|
||||
|
||||
// ❌ Avoid
|
||||
(window as any).umami?.track("button_click", { button_id: "cta" });
|
||||
```
|
||||
|
||||
### 2. Check Environment
|
||||
|
||||
The service layer automatically handles environment detection:
|
||||
|
||||
```typescript
|
||||
// ✅ Safe - works in both server and client
|
||||
const services = getAppServices();
|
||||
services.analytics.track("event", { prop: "value" });
|
||||
|
||||
// ❌ Unsafe - may fail in server environment
|
||||
if (typeof window !== "undefined") {
|
||||
window.umami?.track("event", { prop: "value" });
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Type-Safe Events
|
||||
|
||||
Import events from the centralized definitions:
|
||||
|
||||
```typescript
|
||||
import { AnalyticsEvents } from "@/components/analytics/analytics-events";
|
||||
|
||||
// ✅ Type-safe
|
||||
services.analytics.track(AnalyticsEvents.BUTTON_CLICK, {
|
||||
button_id: "cta",
|
||||
});
|
||||
|
||||
// ❌ Prone to typos
|
||||
services.analytics.track("button_click", {
|
||||
button_id: "cta",
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Handle Disabled Analytics
|
||||
|
||||
The service layer gracefully handles disabled analytics:
|
||||
|
||||
```typescript
|
||||
// When NEXT_PUBLIC_UMAMI_WEBSITE_ID is not set:
|
||||
// - NoopAnalyticsService is used
|
||||
// - All calls are safe (no-op)
|
||||
// - No errors are thrown
|
||||
|
||||
const services = getAppServices();
|
||||
services.analytics.track("event", { prop: "value" }); // Safe, does nothing
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Mocking for Tests
|
||||
|
||||
```typescript
|
||||
// __tests__/analytics-mock.ts
|
||||
export const mockAnalytics = {
|
||||
track: jest.fn(),
|
||||
trackPageview: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock("@/lib/services/create-services", () => ({
|
||||
getAppServices: () => ({
|
||||
analytics: mockAnalytics,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Usage in tests
|
||||
import { mockAnalytics } from "./analytics-mock";
|
||||
|
||||
test("tracks button click", () => {
|
||||
// ... test code ...
|
||||
expect(mockAnalytics.track).toHaveBeenCalledWith("button_click", {
|
||||
button_id: "cta",
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Development Mode
|
||||
|
||||
In development, the service layer logs to console:
|
||||
|
||||
```bash
|
||||
# Console output:
|
||||
[Umami] Tracked event: button_click { button_id: 'cta' }
|
||||
[Umami] Tracked pageview: /products/123
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The service layer includes built-in error handling:
|
||||
|
||||
1. **Environment Detection** - Checks for browser environment
|
||||
2. **Service Availability** - Checks if Umami is loaded
|
||||
3. **Graceful Degradation** - Falls back to NoopAnalyticsService if needed
|
||||
|
||||
```typescript
|
||||
// These are all safe:
|
||||
const services = getAppServices();
|
||||
services.analytics.track("event", { prop: "value" }); // Works or does nothing
|
||||
services.analytics.trackPageview("/path"); // Works or does nothing
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
### Singleton Pattern
|
||||
|
||||
The service layer uses a singleton pattern for performance:
|
||||
|
||||
```typescript
|
||||
// First call creates the singleton
|
||||
const services1 = getAppServices();
|
||||
|
||||
// Subsequent calls return the cached singleton
|
||||
const services2 = getAppServices();
|
||||
|
||||
// services1 === services2 (same instance)
|
||||
```
|
||||
|
||||
### Lazy Initialization
|
||||
|
||||
Services are only created when first accessed:
|
||||
|
||||
```typescript
|
||||
// Services are not created until getAppServices() is called
|
||||
// This keeps initial bundle size minimal
|
||||
```
|
||||
|
||||
## Integration with Components
|
||||
|
||||
### Client Components
|
||||
|
||||
```typescript
|
||||
'use client';
|
||||
|
||||
import { getAppServices } from '@/lib/services/create-services';
|
||||
|
||||
function MyComponent() {
|
||||
const handleClick = () => {
|
||||
const services = getAppServices();
|
||||
services.analytics.track('button_click', { button_id: 'my-button' });
|
||||
};
|
||||
|
||||
return <button onClick={handleClick}>Click Me</button>;
|
||||
}
|
||||
```
|
||||
|
||||
### Server Components
|
||||
|
||||
```typescript
|
||||
import { getAppServices } from '@/lib/services/create-services';
|
||||
|
||||
async function MyServerComponent() {
|
||||
const services = getAppServices();
|
||||
|
||||
// Note: Analytics won't work in server components
|
||||
// Use client components for analytics tracking
|
||||
// But you can still access other services like cache
|
||||
|
||||
const data = await services.cache.get('key');
|
||||
|
||||
return <div>{data}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Analytics Not Working
|
||||
|
||||
1. **Check environment variables:**
|
||||
|
||||
```bash
|
||||
echo $NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||
```
|
||||
|
||||
2. **Verify service selection:**
|
||||
|
||||
```typescript
|
||||
import { getAppServices } from "@/lib/services/create-services";
|
||||
|
||||
const services = getAppServices();
|
||||
console.log(services.analytics); // Should be UmamiAnalyticsService
|
||||
```
|
||||
|
||||
3. **Check Umami dashboard:**
|
||||
- Log into Umami
|
||||
- Verify website ID matches
|
||||
- Check if data is being received
|
||||
|
||||
### Common Issues
|
||||
|
||||
| Issue | Solution |
|
||||
| ------------------- | ----------------------------------- |
|
||||
| No data in Umami | Check website ID and script URL |
|
||||
| Events not tracking | Verify service is being used |
|
||||
| Script not loading | Check network connection, CORS |
|
||||
| Wrong data | Verify event properties are correct |
|
||||
|
||||
## Related Files
|
||||
|
||||
- [`components/analytics/useAnalytics.ts`](../components/analytics/useAnalytics.ts) - Custom hook for easy event tracking
|
||||
- [`components/analytics/analytics-events.ts`](../components/analytics/analytics-events.ts) - Event definitions
|
||||
- [`components/analytics/UmamiScript.tsx`](../components/analytics/UmamiScript.tsx) - Script loader component
|
||||
- [`components/analytics/AnalyticsProvider.tsx`](../components/analytics/AnalyticsProvider.tsx) - Route change tracker
|
||||
- [`lib/services/create-services.ts`](../lib/services/create-services.ts) - Service factory
|
||||
|
||||
## Summary
|
||||
|
||||
The analytics service layer provides:
|
||||
|
||||
- ✅ **Type-safe API** - TypeScript throughout
|
||||
- ✅ **Clean abstraction** - Easy to switch analytics providers
|
||||
- ✅ **Graceful degradation** - Safe no-op fallback
|
||||
- ✅ **Comprehensive documentation** - JSDoc comments and examples
|
||||
- ✅ **Performance optimized** - Singleton pattern, lazy initialization
|
||||
- ✅ **Error handling** - Safe in all environments
|
||||
|
||||
This layer is the foundation for all analytics tracking in the application.
|
||||
@@ -1,3 +1,76 @@
|
||||
/**
|
||||
* Type definition for analytics event properties.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const properties: AnalyticsEventProperties = {
|
||||
* product_id: '123',
|
||||
* product_name: 'Cable',
|
||||
* price: 99.99,
|
||||
* quantity: 1,
|
||||
* in_stock: true,
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export type AnalyticsEventProperties = Record<
|
||||
string,
|
||||
string | number | boolean | null | undefined
|
||||
>;
|
||||
|
||||
/**
|
||||
* Interface for analytics service implementations.
|
||||
*
|
||||
* This interface defines the contract for all analytics services,
|
||||
* allowing for different implementations (Umami, Google Analytics, etc.)
|
||||
* while maintaining a consistent API.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Using the service directly
|
||||
* const service = new UmamiAnalyticsService({ enabled: true });
|
||||
* service.track('button_click', { button_id: 'cta' });
|
||||
* service.trackPageview('/products/123');
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Using the useAnalytics hook (recommended)
|
||||
* const { trackEvent, trackPageview } = useAnalytics();
|
||||
* trackEvent('button_click', { button_id: 'cta' });
|
||||
* trackPageview('/products/123');
|
||||
* ```
|
||||
*/
|
||||
export interface AnalyticsService {
|
||||
trackEvent(name: string, properties?: Record<string, unknown>): void;
|
||||
/**
|
||||
* Track a custom event with optional properties.
|
||||
*
|
||||
* @param eventName - The name of the event to track
|
||||
* @param props - Optional event properties (metadata)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* track('product_add_to_cart', {
|
||||
* product_id: '123',
|
||||
* product_name: 'Cable',
|
||||
* price: 99.99,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
track(eventName: string, props?: AnalyticsEventProperties): void;
|
||||
|
||||
/**
|
||||
* Track a pageview.
|
||||
*
|
||||
* @param url - The URL to track (defaults to current location)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Track current page
|
||||
* trackPageview();
|
||||
*
|
||||
* // Track custom URL
|
||||
* trackPageview('/products/123?category=cables');
|
||||
* ```
|
||||
*/
|
||||
trackPageview(url?: string): void;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,71 @@
|
||||
import type { AnalyticsService } from "./analytics-service";
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import type {
|
||||
AnalyticsEventProperties,
|
||||
AnalyticsService,
|
||||
} from "./analytics-service";
|
||||
|
||||
/**
|
||||
* No-op Analytics Service Implementation.
|
||||
*
|
||||
* This service implements the AnalyticsService interface but does nothing.
|
||||
* It's used as a fallback when analytics are disabled or not configured.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Service creation (usually done by create-services.ts)
|
||||
* const service = new NoopAnalyticsService();
|
||||
*
|
||||
* // These calls do nothing but are safe to execute
|
||||
* service.track('button_click', { button_id: 'cta' });
|
||||
* service.trackPageview('/products/123');
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Automatic fallback in create-services.ts
|
||||
* const umamiEnabled = Boolean(process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID);
|
||||
* const analytics = umamiEnabled
|
||||
* ? new UmamiAnalyticsService({ enabled: true })
|
||||
* : new NoopAnalyticsService(); // Fallback when no website ID
|
||||
* ```
|
||||
*/
|
||||
export class NoopAnalyticsService implements AnalyticsService {
|
||||
trackEvent() {}
|
||||
/**
|
||||
* No-op implementation of track.
|
||||
*
|
||||
* This method does nothing but maintains the same signature as other
|
||||
* analytics services for consistency.
|
||||
*
|
||||
* @param _eventName - Event name (ignored)
|
||||
* @param _props - Event properties (ignored)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Safe to call even when analytics are disabled
|
||||
* service.track('button_click', { button_id: 'cta' });
|
||||
* // No error, no action taken
|
||||
* ```
|
||||
*/
|
||||
track(_eventName: string, _props?: AnalyticsEventProperties) {
|
||||
// intentionally noop - analytics are disabled
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op implementation of trackPageview.
|
||||
*
|
||||
* This method does nothing but maintains the same signature as other
|
||||
* analytics services for consistency.
|
||||
*
|
||||
* @param _url - URL to track (ignored)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Safe to call even when analytics are disabled
|
||||
* service.trackPageview('/products/123');
|
||||
* // No error, no action taken
|
||||
* ```
|
||||
*/
|
||||
trackPageview(_url?: string) {
|
||||
// intentionally noop - analytics are disabled
|
||||
}
|
||||
}
|
||||
|
||||
112
lib/services/analytics/umami-analytics-service.ts
Normal file
112
lib/services/analytics/umami-analytics-service.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import type {
|
||||
AnalyticsEventProperties,
|
||||
AnalyticsService,
|
||||
} from "./analytics-service";
|
||||
import { config } from "../../config";
|
||||
|
||||
/**
|
||||
* Configuration options for UmamiAnalyticsService.
|
||||
*
|
||||
* @property enabled - Whether analytics are enabled
|
||||
*/
|
||||
export type UmamiAnalyticsServiceOptions = {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Umami Analytics Service Implementation (Script-less/Proxy edition).
|
||||
*
|
||||
* This version implements the Umami tracking protocol directly via fetch,
|
||||
* eliminating the need to load an external script.js file.
|
||||
*
|
||||
* In the browser, it gathers standard metadata (screen, language, referrer)
|
||||
* and sends it to the proxied '/stats/api/send' endpoint.
|
||||
*/
|
||||
export class UmamiAnalyticsService implements AnalyticsService {
|
||||
private websiteId?: string;
|
||||
private endpoint: string;
|
||||
|
||||
constructor(private readonly options: UmamiAnalyticsServiceOptions) {
|
||||
this.websiteId = config.analytics.umami.websiteId;
|
||||
|
||||
// On server, use the full internal URL; on client, use the proxied path
|
||||
this.endpoint =
|
||||
typeof window === "undefined"
|
||||
? config.analytics.umami.apiEndpoint
|
||||
: "/stats";
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to send the payload to Umami API.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private async sendPayload(type: "event", data: Record<string, unknown>) {
|
||||
if (!this.options.enabled || !this.websiteId) return;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
website: this.websiteId,
|
||||
hostname:
|
||||
typeof window !== "undefined" ? window.location.hostname : "server",
|
||||
screen:
|
||||
typeof window !== "undefined"
|
||||
? `${window.screen.width}x${window.screen.height}`
|
||||
: undefined,
|
||||
language:
|
||||
typeof window !== "undefined" ? navigator.language : undefined,
|
||||
referrer: typeof window !== "undefined" ? document.referrer : undefined,
|
||||
...data,
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.endpoint}/api/send`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent":
|
||||
typeof window === "undefined" ? "KLZ-Server" : navigator.userAgent,
|
||||
},
|
||||
body: JSON.stringify({ type, payload }),
|
||||
keepalive: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
if (!response.ok && process.env.NODE_ENV === "development") {
|
||||
const errorText = await response.text();
|
||||
console.warn(
|
||||
`[Umami] API responded with ${response.status}: ${errorText}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("[Umami] Failed to send analytics:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a custom event.
|
||||
*/
|
||||
track(eventName: string, props?: AnalyticsEventProperties) {
|
||||
this.sendPayload("event", {
|
||||
name: eventName,
|
||||
data: props,
|
||||
url:
|
||||
typeof window !== "undefined"
|
||||
? window.location.pathname + window.location.search
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a pageview.
|
||||
*/
|
||||
trackPageview(url?: string) {
|
||||
this.sendPayload("event", {
|
||||
url:
|
||||
url ||
|
||||
(typeof window !== "undefined"
|
||||
? window.location.pathname + window.location.search
|
||||
: undefined),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import type { ErrorReportingService } from "./errors/error-reporting-service";
|
||||
import type { LoggerService } from "./logging/logger-service";
|
||||
import type { NotificationService } from "./notifications/notification-service";
|
||||
|
||||
// Simple constructor-based DI container.
|
||||
export class AppServices {
|
||||
constructor(
|
||||
public readonly analytics: AnalyticsService,
|
||||
|
||||
10
lib/services/cache/cache-service.ts
vendored
10
lib/services/cache/cache-service.ts
vendored
@@ -1,5 +1,9 @@
|
||||
export type CacheSetOptions = {
|
||||
ttlSeconds?: number;
|
||||
};
|
||||
|
||||
export interface CacheService {
|
||||
get<T>(key: string): Promise<T | null>;
|
||||
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
get<T>(key: string): Promise<T | undefined>;
|
||||
set<T>(key: string, value: T, options?: CacheSetOptions): Promise<void>;
|
||||
del(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
36
lib/services/cache/memory-cache-service.ts
vendored
36
lib/services/cache/memory-cache-service.ts
vendored
@@ -1,26 +1,30 @@
|
||||
import type { CacheService } from "./cache-service";
|
||||
import type { CacheService, CacheSetOptions } from "./cache-service";
|
||||
|
||||
type Entry = {
|
||||
value: unknown;
|
||||
expiresAt?: number;
|
||||
};
|
||||
|
||||
export class MemoryCacheService implements CacheService {
|
||||
private cache = new Map<string, { value: any; expiry: number | null }>();
|
||||
private readonly store = new Map<string, Entry>();
|
||||
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
const item = this.cache.get(key);
|
||||
if (!item) return null;
|
||||
|
||||
if (item.expiry && item.expiry < Date.now()) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
async get<T>(key: string) {
|
||||
const entry = this.store.get(key);
|
||||
if (!entry) return undefined;
|
||||
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
||||
this.store.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return item.value as T;
|
||||
return entry.value as T;
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T, ttlSeconds?: number): Promise<void> {
|
||||
const expiry = ttlSeconds ? Date.now() + ttlSeconds * 1000 : null;
|
||||
this.cache.set(key, { value, expiry });
|
||||
async set<T>(key: string, value: T, options?: CacheSetOptions) {
|
||||
const ttl = options?.ttlSeconds;
|
||||
const expiresAt = ttl ? Date.now() + ttl * 1000 : undefined;
|
||||
this.store.set(key, { value, expiresAt });
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
this.cache.delete(key);
|
||||
async del(key: string) {
|
||||
this.store.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { AppServices } from "./app-services";
|
||||
import { NoopAnalyticsService } from "./analytics/noop-analytics-service";
|
||||
import { UmamiAnalyticsService } from "./analytics/umami-analytics-service";
|
||||
import { MemoryCacheService } from "./cache/memory-cache-service";
|
||||
import { GlitchtipErrorReportingService } from "./errors/glitchtip-error-reporting-service";
|
||||
import { NoopErrorReportingService } from "./errors/noop-error-reporting-service";
|
||||
import { GotifyNotificationService } from "./notifications/gotify-notification-service";
|
||||
import { NoopNotificationService } from "./notifications/noop-notification-service";
|
||||
import {
|
||||
GotifyNotificationService,
|
||||
NoopNotificationService,
|
||||
} from "./notifications/gotify-notification-service";
|
||||
import { PinoLoggerService } from "./logging/pino-logger-service";
|
||||
import { config, getMaskedConfig } from "../config";
|
||||
|
||||
let singleton: AppServices | undefined;
|
||||
|
||||
export function getServerAppServices(): AppServices {
|
||||
if (singleton) return singleton;
|
||||
|
||||
// Create logger first to log initialization
|
||||
const logger = new PinoLoggerService("server");
|
||||
|
||||
logger.info("Initializing server application services", {
|
||||
@@ -20,7 +23,22 @@ export function getServerAppServices(): AppServices {
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const analytics = new NoopAnalyticsService();
|
||||
logger.info("Service configuration", {
|
||||
umamiEnabled: config.analytics.umami.enabled,
|
||||
sentryEnabled: config.errors.glitchtip.enabled,
|
||||
mailEnabled: Boolean(config.mail.host && config.mail.user),
|
||||
gotifyEnabled: config.notifications.gotify.enabled,
|
||||
});
|
||||
|
||||
const analytics = config.analytics.umami.enabled
|
||||
? new UmamiAnalyticsService({ enabled: true })
|
||||
: new NoopAnalyticsService();
|
||||
|
||||
if (config.analytics.umami.enabled) {
|
||||
logger.info("Umami analytics service initialized");
|
||||
} else {
|
||||
logger.info("Noop analytics service initialized (analytics disabled)");
|
||||
}
|
||||
|
||||
const notifications = config.notifications.gotify.enabled
|
||||
? new GotifyNotificationService({
|
||||
@@ -30,11 +48,35 @@ export function getServerAppServices(): AppServices {
|
||||
})
|
||||
: new NoopNotificationService();
|
||||
|
||||
if (config.notifications.gotify.enabled) {
|
||||
logger.info("Gotify notification service initialized");
|
||||
} else {
|
||||
logger.info(
|
||||
"Noop notification service initialized (notifications disabled)",
|
||||
);
|
||||
}
|
||||
|
||||
const errors = config.errors.glitchtip.enabled
|
||||
? new GlitchtipErrorReportingService({ enabled: true }, notifications)
|
||||
: new NoopErrorReportingService();
|
||||
|
||||
if (config.errors.glitchtip.enabled) {
|
||||
logger.info("GlitchTip error reporting service initialized", {
|
||||
dsnPresent: Boolean(config.errors.glitchtip.dsn),
|
||||
});
|
||||
} else {
|
||||
logger.info(
|
||||
"Noop error reporting service initialized (error reporting disabled)",
|
||||
);
|
||||
}
|
||||
|
||||
const cache = new MemoryCacheService();
|
||||
logger.info("Memory cache service initialized");
|
||||
|
||||
logger.info("Pino logger service initialized", {
|
||||
name: "server",
|
||||
level: config.logging.level,
|
||||
});
|
||||
|
||||
singleton = new AppServices(analytics, errors, cache, logger, notifications);
|
||||
|
||||
|
||||
154
lib/services/create-services.ts
Normal file
154
lib/services/create-services.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { AppServices } from "./app-services";
|
||||
import { NoopAnalyticsService } from "./analytics/noop-analytics-service";
|
||||
import { MemoryCacheService } from "./cache/memory-cache-service";
|
||||
import { GlitchtipErrorReportingService } from "./errors/glitchtip-error-reporting-service";
|
||||
import { NoopErrorReportingService } from "./errors/noop-error-reporting-service";
|
||||
import { NoopLoggerService } from "./logging/noop-logger-service";
|
||||
import { PinoLoggerService } from "./logging/pino-logger-service";
|
||||
import { NoopNotificationService } from "./notifications/gotify-notification-service";
|
||||
import { config, getMaskedConfig } from "../config";
|
||||
|
||||
/**
|
||||
* Singleton instance of AppServices.
|
||||
*
|
||||
* In Next.js, module singletons are per-process (server) and per-tab (client).
|
||||
* This is sufficient for a small service layer and provides better performance
|
||||
* than creating new instances on every request.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
let singleton: AppServices | undefined;
|
||||
|
||||
/**
|
||||
* Get the application services singleton.
|
||||
*
|
||||
* This function creates and caches the application services, including:
|
||||
* - Analytics service (Umami or no-op)
|
||||
* - Error reporting service (GlitchTip/Sentry or no-op)
|
||||
* - Cache service (in-memory)
|
||||
*
|
||||
* The services are configured based on environment variables:
|
||||
* - `UMAMI_WEBSITE_ID` - Enables Umami analytics
|
||||
* - `NEXT_PUBLIC_SENTRY_DSN` - Enables client-side error reporting
|
||||
* - `SENTRY_DSN` - Enables server-side error reporting
|
||||
*
|
||||
* @returns {AppServices} The application services singleton
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get services in a client component
|
||||
* import { getAppServices } from '@/lib/services/create-services';
|
||||
*
|
||||
* const services = getAppServices();
|
||||
* services.analytics.track('button_click', { button_id: 'cta' });
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get services in a server component or API route
|
||||
* import { getAppServices } from '@/lib/services/create-services';
|
||||
*
|
||||
* const services = getAppServices();
|
||||
* await services.cache.set('key', 'value');
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Automatic service selection based on environment
|
||||
* // If NEXT_PUBLIC_UMAMI_WEBSITE_ID is set:
|
||||
* // services.analytics = UmamiAnalyticsService
|
||||
* // If not set:
|
||||
* // services.analytics = NoopAnalyticsService (safe no-op)
|
||||
* ```
|
||||
*
|
||||
* @see {@link UmamiAnalyticsService} for analytics implementation
|
||||
* @see {@link NoopAnalyticsService} for no-op fallback
|
||||
* @see {@link GlitchtipErrorReportingService} for error reporting
|
||||
* @see {@link MemoryCacheService} for caching
|
||||
*/
|
||||
export function getAppServices(): AppServices {
|
||||
// Return cached instance if available
|
||||
if (singleton) return singleton;
|
||||
|
||||
// Create logger first to log initialization
|
||||
const logger =
|
||||
typeof window === "undefined"
|
||||
? new PinoLoggerService("server")
|
||||
: new NoopLoggerService();
|
||||
|
||||
// Log initialization
|
||||
if (typeof window === "undefined") {
|
||||
// Server-side
|
||||
logger.info("Initializing server application services", {
|
||||
environment: getMaskedConfig(),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
// Client-side
|
||||
logger.info("Initializing client application services", {
|
||||
environment: getMaskedConfig(),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Determine which services to enable based on environment variables
|
||||
const umamiEnabled = config.analytics.umami.enabled;
|
||||
const sentryEnabled = config.errors.glitchtip.enabled;
|
||||
|
||||
logger.info("Service configuration", {
|
||||
umamiEnabled,
|
||||
sentryEnabled,
|
||||
isServer: typeof window === "undefined",
|
||||
});
|
||||
|
||||
// Create analytics service (Umami or no-op)
|
||||
// Use dynamic import to avoid importing server-only code in client components
|
||||
const analytics = umamiEnabled
|
||||
? (() => {
|
||||
const { UmamiAnalyticsService } =
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require("./analytics/umami-analytics-service");
|
||||
return new UmamiAnalyticsService({ enabled: true });
|
||||
})()
|
||||
: new NoopAnalyticsService();
|
||||
|
||||
if (umamiEnabled) {
|
||||
logger.info("Umami analytics service initialized");
|
||||
} else {
|
||||
logger.info("Noop analytics service initialized (analytics disabled)");
|
||||
}
|
||||
|
||||
// Create error reporting service (GlitchTip/Sentry or no-op)
|
||||
const errors = sentryEnabled
|
||||
? new GlitchtipErrorReportingService({ enabled: true })
|
||||
: new NoopErrorReportingService();
|
||||
|
||||
if (sentryEnabled) {
|
||||
logger.info(
|
||||
`GlitchTip error reporting service initialized (${typeof window === "undefined" ? "server" : "client"})`,
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
"Noop error reporting service initialized (error reporting disabled)",
|
||||
);
|
||||
}
|
||||
|
||||
// IMPORTANT: This module is imported by client components.
|
||||
// Do not import Node-only modules (like the `redis` client) here.
|
||||
// Use [`getServerAppServices()`](lib/services/create-services.server.ts:1) on the server.
|
||||
const cache = new MemoryCacheService();
|
||||
logger.info("Memory cache service initialized");
|
||||
|
||||
logger.info("Pino logger service initialized", {
|
||||
name: typeof window === "undefined" ? "server" : "client",
|
||||
level: config.logging.level,
|
||||
});
|
||||
|
||||
// Create and cache the singleton
|
||||
const notifications = new NoopNotificationService();
|
||||
singleton = new AppServices(analytics, errors, cache, logger, notifications);
|
||||
|
||||
logger.info("All application services initialized successfully");
|
||||
|
||||
return singleton;
|
||||
}
|
||||
@@ -1,4 +1,27 @@
|
||||
export type ErrorReportingUser = {
|
||||
id?: string;
|
||||
email?: string;
|
||||
username?: string;
|
||||
};
|
||||
|
||||
export type ErrorReportingLevel =
|
||||
| "fatal"
|
||||
| "error"
|
||||
| "warning"
|
||||
| "info"
|
||||
| "debug"
|
||||
| "log";
|
||||
|
||||
export interface ErrorReportingService {
|
||||
captureException(error: unknown, context?: Record<string, unknown>): void;
|
||||
captureMessage(message: string, context?: Record<string, unknown>): void;
|
||||
captureException(
|
||||
error: unknown,
|
||||
context?: Record<string, unknown>,
|
||||
): Promise<string | undefined> | string | undefined;
|
||||
captureMessage(
|
||||
message: string,
|
||||
level?: ErrorReportingLevel,
|
||||
): Promise<string | undefined> | string | undefined;
|
||||
setUser(user: ErrorReportingUser | null): void;
|
||||
setTag(key: string, value: string): void;
|
||||
withScope<T>(fn: () => T, context?: Record<string, unknown>): T;
|
||||
}
|
||||
|
||||
@@ -1,48 +1,77 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import type { ErrorReportingService } from "./error-reporting-service";
|
||||
import type {
|
||||
ErrorReportingLevel,
|
||||
ErrorReportingService,
|
||||
ErrorReportingUser,
|
||||
} from "./error-reporting-service";
|
||||
import type { NotificationService } from "../notifications/notification-service";
|
||||
|
||||
export interface GlitchtipConfig {
|
||||
enabled: boolean;
|
||||
}
|
||||
type SentryLike = typeof Sentry;
|
||||
|
||||
export type GlitchtipErrorReportingServiceOptions = {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
// GlitchTip speaks the Sentry protocol; @sentry/nextjs can send to GlitchTip via DSN.
|
||||
export class GlitchtipErrorReportingService implements ErrorReportingService {
|
||||
constructor(
|
||||
private readonly config: GlitchtipConfig,
|
||||
private readonly options: GlitchtipErrorReportingServiceOptions,
|
||||
private readonly notifications?: NotificationService,
|
||||
private readonly sentry: SentryLike = Sentry,
|
||||
) {}
|
||||
|
||||
captureException(error: unknown, context?: Record<string, unknown>) {
|
||||
if (!this.config.enabled) return;
|
||||
|
||||
Sentry.withScope((scope) => {
|
||||
if (context) {
|
||||
scope.setExtras(context);
|
||||
}
|
||||
Sentry.captureException(error);
|
||||
});
|
||||
async captureException(error: unknown, context?: Record<string, unknown>) {
|
||||
if (!this.options.enabled) return undefined;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result = this.sentry.captureException(error, context as any) as any;
|
||||
|
||||
// Send to Gotify if it's considered critical or if we just want all exceptions there
|
||||
// For now, let's send all exceptions to Gotify as requested "notify me via gotify about critical error messages"
|
||||
// We'll treat all captureException calls as potentially critical or at least noteworthy
|
||||
if (this.notifications) {
|
||||
this.notifications
|
||||
.notify({
|
||||
title: "🚨 Exception Captured",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
priority: 10,
|
||||
})
|
||||
.catch((err) =>
|
||||
console.error("Failed to send notification for exception", err),
|
||||
);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
const contextStr = context
|
||||
? `\nContext: ${JSON.stringify(context, null, 2)}`
|
||||
: "";
|
||||
|
||||
await this.notifications.notify({
|
||||
title: "🔥 Critical Error Captured",
|
||||
message: `Error: ${errorMessage}${contextStr}`,
|
||||
priority: 7,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
captureMessage(message: string, context?: Record<string, unknown>) {
|
||||
if (!this.config.enabled) return;
|
||||
captureMessage(message: string, level: ErrorReportingLevel = "error") {
|
||||
if (!this.options.enabled) return undefined;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return this.sentry.captureMessage(message, level as any) as any;
|
||||
}
|
||||
|
||||
Sentry.withScope((scope) => {
|
||||
setUser(user: ErrorReportingUser | null) {
|
||||
if (!this.options.enabled) return;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.sentry.setUser(user as any);
|
||||
}
|
||||
|
||||
setTag(key: string, value: string) {
|
||||
if (!this.options.enabled) return;
|
||||
this.sentry.setTag(key, value);
|
||||
}
|
||||
|
||||
withScope<T>(fn: () => T, context?: Record<string, unknown>) {
|
||||
if (!this.options.enabled) return fn();
|
||||
|
||||
return this.sentry.withScope((scope) => {
|
||||
if (context) {
|
||||
scope.setExtras(context);
|
||||
for (const [key, value] of Object.entries(context)) {
|
||||
scope.setExtra(key, value);
|
||||
}
|
||||
}
|
||||
Sentry.captureMessage(message);
|
||||
return fn();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
import type { ErrorReportingService } from "./error-reporting-service";
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import type {
|
||||
ErrorReportingLevel,
|
||||
ErrorReportingService,
|
||||
ErrorReportingUser,
|
||||
} from "./error-reporting-service";
|
||||
|
||||
export class NoopErrorReportingService implements ErrorReportingService {
|
||||
captureException() {}
|
||||
captureMessage() {}
|
||||
async captureException(_error: unknown, _context?: Record<string, unknown>) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async captureMessage(_message: string, _level?: ErrorReportingLevel) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setUser(_user: ErrorReportingUser | null) {}
|
||||
setTag(_key: string, _value: string) {}
|
||||
|
||||
withScope<T>(fn: () => T, _context?: Record<string, unknown>) {
|
||||
return fn();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
||||
|
||||
export interface LoggerService {
|
||||
debug(message: string, context?: Record<string, unknown>): void;
|
||||
info(message: string, context?: Record<string, unknown>): void;
|
||||
warn(message: string, context?: Record<string, unknown>): void;
|
||||
error(message: string, context?: Record<string, unknown>): void;
|
||||
child(context: Record<string, unknown>): LoggerService;
|
||||
trace(msg: string, ...args: unknown[]): void;
|
||||
debug(msg: string, ...args: unknown[]): void;
|
||||
info(msg: string, ...args: unknown[]): void;
|
||||
warn(msg: string, ...args: unknown[]): void;
|
||||
error(msg: string, ...args: unknown[]): void;
|
||||
fatal(msg: string, ...args: unknown[]): void;
|
||||
child(bindings: Record<string, unknown>): LoggerService;
|
||||
}
|
||||
|
||||
13
lib/services/logging/noop-logger-service.ts
Normal file
13
lib/services/logging/noop-logger-service.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { LoggerService } from "./logger-service";
|
||||
|
||||
export class NoopLoggerService implements LoggerService {
|
||||
trace() {}
|
||||
debug() {}
|
||||
info() {}
|
||||
warn() {}
|
||||
error() {}
|
||||
fatal() {}
|
||||
child() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import { pino, type Logger as PinoLogger } from "pino";
|
||||
import pino, { Logger as PinoLogger } from "pino";
|
||||
import type { LoggerService } from "./logger-service";
|
||||
import { config } from "../../config";
|
||||
|
||||
export class PinoLoggerService implements LoggerService {
|
||||
private logger: PinoLogger;
|
||||
private readonly logger: PinoLogger;
|
||||
|
||||
constructor(name?: string, parent?: PinoLogger) {
|
||||
if (parent) {
|
||||
this.logger = parent.child({ name });
|
||||
} else {
|
||||
// In Next.js, especially in the Edge runtime or during instrumentation,
|
||||
// pino transports (which use worker threads) can cause issues.
|
||||
// We disable transport in production and during instrumentation.
|
||||
const useTransport =
|
||||
config.isDevelopment && typeof window === "undefined";
|
||||
|
||||
@@ -27,30 +30,41 @@ export class PinoLoggerService implements LoggerService {
|
||||
}
|
||||
}
|
||||
|
||||
debug(message: string, context?: Record<string, unknown>) {
|
||||
if (context) this.logger.debug(context, message);
|
||||
else this.logger.debug(message);
|
||||
trace(msg: string, ...args: unknown[]) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.logger.trace(msg, ...(args as any));
|
||||
}
|
||||
|
||||
info(message: string, context?: Record<string, unknown>) {
|
||||
if (context) this.logger.info(context, message);
|
||||
else this.logger.info(message);
|
||||
debug(msg: string, ...args: unknown[]) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.logger.debug(msg, ...(args as any));
|
||||
}
|
||||
|
||||
warn(message: string, context?: Record<string, unknown>) {
|
||||
if (context) this.logger.warn(context, message);
|
||||
else this.logger.warn(message);
|
||||
info(msg: string, ...args: unknown[]) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.logger.info(msg, ...(args as any));
|
||||
}
|
||||
|
||||
error(message: string, context?: Record<string, unknown>) {
|
||||
if (context) this.logger.error(context, message);
|
||||
else this.logger.error(message);
|
||||
warn(msg: string, ...args: unknown[]) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.logger.warn(msg, ...(args as any));
|
||||
}
|
||||
|
||||
child(context: Record<string, unknown>): LoggerService {
|
||||
const childPino = this.logger.child(context);
|
||||
error(msg: string, ...args: unknown[]) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.logger.error(msg, ...(args as any));
|
||||
}
|
||||
|
||||
fatal(msg: string, ...args: unknown[]) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.logger.fatal(msg, ...(args as any));
|
||||
}
|
||||
|
||||
child(bindings: Record<string, unknown>): LoggerService {
|
||||
const childPino = this.logger.child(bindings);
|
||||
const service = new PinoLoggerService();
|
||||
service.logger = childPino;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(service as any).logger = childPino;
|
||||
return service;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {
|
||||
NotificationMessage,
|
||||
import {
|
||||
NotificationOptions,
|
||||
NotificationService,
|
||||
} from "./notification-service";
|
||||
|
||||
@@ -10,35 +10,44 @@ export interface GotifyConfig {
|
||||
}
|
||||
|
||||
export class GotifyNotificationService implements NotificationService {
|
||||
constructor(private readonly config: GotifyConfig) {}
|
||||
constructor(private config: GotifyConfig) {}
|
||||
|
||||
async notify(message: NotificationMessage): Promise<void> {
|
||||
async notify(options: NotificationOptions): Promise<void> {
|
||||
if (!this.config.enabled) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${this.config.url}/message?token=${this.config.token}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: message.title,
|
||||
message: message.message,
|
||||
priority: message.priority ?? 5,
|
||||
}),
|
||||
const { title, message, priority = 4 } = options;
|
||||
const url = new URL("message", this.config.url);
|
||||
url.searchParams.set("token", this.config.token);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
message,
|
||||
priority,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(
|
||||
"Failed to send Gotify notification",
|
||||
await response.text(),
|
||||
);
|
||||
const errorText = await response.text();
|
||||
console.error("Gotify notification failed:", {
|
||||
status: response.status,
|
||||
error: errorText,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending Gotify notification", error);
|
||||
console.error("Gotify notification error:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NoopNotificationService implements NotificationService {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async notify(_options: NotificationOptions): Promise<void> {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import type { NotificationService } from "./notification-service";
|
||||
|
||||
export class NoopNotificationService implements NotificationService {
|
||||
async notify() {}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
export interface NotificationMessage {
|
||||
export interface NotificationOptions {
|
||||
title: string;
|
||||
message: string;
|
||||
priority?: number; // 0-10, Gotify style
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
export interface NotificationService {
|
||||
notify(message: NotificationMessage): Promise<void>;
|
||||
notify(options: NotificationOptions): Promise<void>;
|
||||
}
|
||||
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
import withMintelConfig from "@mintel/next-config";
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
const nextConfig = {
|
||||
async rewrites() {
|
||||
const umamiUrl =
|
||||
process.env.UMAMI_API_ENDPOINT ||
|
||||
process.env.UMAMI_SCRIPT_URL ||
|
||||
process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL ||
|
||||
"https://analytics.infra.mintel.me";
|
||||
const glitchtipUrl = process.env.SENTRY_DSN
|
||||
? new URL(process.env.SENTRY_DSN).origin
|
||||
: "https://errors.infra.mintel.me";
|
||||
|
||||
return [
|
||||
{
|
||||
source: "/stats/:path*",
|
||||
destination: `${umamiUrl}/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/errors/:path*",
|
||||
destination: `${glitchtipUrl}/:path*`,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default withMintelConfig(nextConfig);
|
||||
|
||||
33
package.json
33
package.json
@@ -2,21 +2,22 @@
|
||||
"name": "mb-grid-solutions.com",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.18.3",
|
||||
"scripts": {
|
||||
"dev": "docker network create infra 2>/dev/null || true && echo '\\n🚀 Development Environment Starting...\\n\\n📱 App: http://mb-grid-solutions.localhost\\n🗄️ CMS: http://cms.mb-grid-solutions.localhost/admin\\n🚦 Traefik: http://localhost:8080\\n\\n(Press Ctrl+C to stop)\\n' && docker compose down --remove-orphans && docker compose up app directus directus-db",
|
||||
"dev:next": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"lint": "eslint app components lib scripts",
|
||||
"test": "vitest",
|
||||
"prepare": "husky",
|
||||
"directus:bootstrap": "DIRECTUS_URL=http://localhost:8055 npx tsx --env-file=.env scripts/setup-directus.ts",
|
||||
"directus:push:staging": "./scripts/sync-directus.sh push staging",
|
||||
"directus:pull:staging": "./scripts/sync-directus.sh pull staging",
|
||||
"directus:push:testing": "./scripts/sync-directus.sh push testing",
|
||||
"directus:pull:testing": "./scripts/sync-directus.sh pull testing",
|
||||
"directus:push:prod": "./scripts/sync-directus.sh push production",
|
||||
"directus:pull:prod": "./scripts/sync-directus.sh pull production",
|
||||
"cms:bootstrap": "DIRECTUS_URL=http://localhost:8055 npx tsx --env-file=.env scripts/setup-directus.ts",
|
||||
"cms:push:staging": "./scripts/sync-directus.sh push staging",
|
||||
"cms:pull:staging": "./scripts/sync-directus.sh pull staging",
|
||||
"cms:push:testing": "./scripts/sync-directus.sh push testing",
|
||||
"cms:pull:testing": "./scripts/sync-directus.sh pull testing",
|
||||
"cms:push:prod": "./scripts/sync-directus.sh push production",
|
||||
"cms:pull:prod": "./scripts/sync-directus.sh pull production",
|
||||
"pagespeed:test": "mintel pagespeed test"
|
||||
},
|
||||
"keywords": [],
|
||||
@@ -24,8 +25,8 @@
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@mintel/next-config": "1.1.13",
|
||||
"@mintel/next-utils": "1.1.13",
|
||||
"@mintel/next-config": "^1.1.13",
|
||||
"@mintel/next-utils": "^1.1.13",
|
||||
"@sentry/nextjs": "^10.38.0",
|
||||
"framer-motion": "^12.29.2",
|
||||
"lucide-react": "^0.562.0",
|
||||
@@ -41,10 +42,10 @@
|
||||
"@commitlint/cli": "^20.4.0",
|
||||
"@commitlint/config-conventional": "^20.4.0",
|
||||
"@directus/sdk": "^21.0.0",
|
||||
"@mintel/cli": "1.1.13",
|
||||
"@mintel/eslint-config": "1.1.13",
|
||||
"@mintel/husky-config": "1.1.13",
|
||||
"@mintel/tsconfig": "1.1.13",
|
||||
"@mintel/cli": "^1.1.13",
|
||||
"@mintel/eslint-config": "^1.1.13",
|
||||
"@mintel/husky-config": "^1.1.13",
|
||||
"@mintel/tsconfig": "^1.1.13",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
@@ -54,8 +55,8 @@
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-next": "^16.1.6",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^27.4.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
|
||||
457
pnpm-lock.yaml
generated
457
pnpm-lock.yaml
generated
@@ -9,10 +9,10 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@mintel/next-config':
|
||||
specifier: 1.1.13
|
||||
specifier: ^1.1.13
|
||||
version: 1.1.13(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.211.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(webpack@5.104.1)
|
||||
'@mintel/next-utils':
|
||||
specifier: 1.1.13
|
||||
specifier: ^1.1.13
|
||||
version: 1.1.13(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
|
||||
'@sentry/nextjs':
|
||||
specifier: ^10.38.0
|
||||
@@ -55,16 +55,16 @@ importers:
|
||||
specifier: ^21.0.0
|
||||
version: 21.0.0
|
||||
'@mintel/cli':
|
||||
specifier: 1.1.13
|
||||
specifier: ^1.1.13
|
||||
version: 1.1.13
|
||||
'@mintel/eslint-config':
|
||||
specifier: 1.1.13
|
||||
version: 1.1.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
specifier: ^1.1.13
|
||||
version: 1.1.13(eslint@8.57.1)(typescript@5.9.3)
|
||||
'@mintel/husky-config':
|
||||
specifier: 1.1.13
|
||||
specifier: ^1.1.13
|
||||
version: 1.1.13
|
||||
'@mintel/tsconfig':
|
||||
specifier: 1.1.13
|
||||
specifier: ^1.1.13
|
||||
version: 1.1.13
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.1.18
|
||||
@@ -94,11 +94,11 @@ importers:
|
||||
specifier: ^10.4.23
|
||||
version: 10.4.24(postcss@8.5.6)
|
||||
eslint:
|
||||
specifier: ^9.39.2
|
||||
version: 9.39.2(jiti@2.6.1)
|
||||
specifier: ^8.57.1
|
||||
version: 8.57.1
|
||||
eslint-config-next:
|
||||
specifier: ^16.1.6
|
||||
version: 16.1.6(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
specifier: 15.1.6
|
||||
version: 15.1.6(eslint@8.57.1)(typescript@5.9.3)
|
||||
husky:
|
||||
specifier: ^9.1.7
|
||||
version: 9.1.7
|
||||
@@ -520,34 +520,22 @@ packages:
|
||||
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
|
||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||
|
||||
'@eslint/config-array@0.21.1':
|
||||
resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@eslint/config-helpers@0.4.2':
|
||||
resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@eslint/core@0.17.0':
|
||||
resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
'@eslint/eslintrc@2.1.4':
|
||||
resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
'@eslint/eslintrc@3.3.3':
|
||||
resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@eslint/js@8.57.1':
|
||||
resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
'@eslint/js@9.39.2':
|
||||
resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@eslint/object-schema@2.1.7':
|
||||
resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@eslint/plugin-kit@0.4.1':
|
||||
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@exodus/bytes@1.11.0':
|
||||
resolution: {integrity: sha512-wO3vd8nsEHdumsXrjGO/v4p6irbg7hy9kvIeR6i2AwylZSk4HJdWgL0FNaVquW1+AweJcdvU1IEpuIWk/WaPnA==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
@@ -575,21 +563,18 @@ packages:
|
||||
'@formatjs/intl-localematcher@0.8.1':
|
||||
resolution: {integrity: sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==}
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
|
||||
'@humanfs/node@0.16.7':
|
||||
resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
'@humanwhocodes/config-array@0.13.0':
|
||||
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
deprecated: Use @eslint/config-array instead
|
||||
|
||||
'@humanwhocodes/module-importer@1.0.1':
|
||||
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
|
||||
engines: {node: '>=12.22'}
|
||||
|
||||
'@humanwhocodes/retry@0.4.3':
|
||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||
engines: {node: '>=18.18'}
|
||||
'@humanwhocodes/object-schema@2.0.3':
|
||||
resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
|
||||
deprecated: Use @eslint/object-schema instead
|
||||
|
||||
'@img/colour@1.0.0':
|
||||
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
|
||||
@@ -887,9 +872,6 @@ packages:
|
||||
'@next/eslint-plugin-next@15.1.6':
|
||||
resolution: {integrity: sha512-+slMxhTgILUntZDGNgsKEYHUvpn72WP1YTlkmEhS51vnVd7S9jEEy0n9YAMcI21vUG4akTw9voWH02lrClt/yw==}
|
||||
|
||||
'@next/eslint-plugin-next@16.1.6':
|
||||
resolution: {integrity: sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==}
|
||||
|
||||
'@next/swc-darwin-arm64@15.1.6':
|
||||
resolution: {integrity: sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -2245,6 +2227,9 @@ packages:
|
||||
resolution: {integrity: sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
|
||||
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
||||
resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
|
||||
cpu: [arm]
|
||||
@@ -2838,6 +2823,10 @@ packages:
|
||||
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
doctrine@3.0.0:
|
||||
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
dom-accessibility-api@0.5.16:
|
||||
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
|
||||
|
||||
@@ -2957,15 +2946,6 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
eslint-config-next@16.1.6:
|
||||
resolution: {integrity: sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==}
|
||||
peerDependencies:
|
||||
eslint: '>=9.0.0'
|
||||
typescript: '>=3.3.1'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
eslint-import-resolver-node@0.3.9:
|
||||
resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
|
||||
|
||||
@@ -3025,12 +3005,6 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
|
||||
|
||||
eslint-plugin-react-hooks@7.0.1:
|
||||
resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
|
||||
|
||||
eslint-plugin-react@7.37.5:
|
||||
resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -3041,9 +3015,9 @@ packages:
|
||||
resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
eslint-scope@8.4.0:
|
||||
resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
eslint-scope@7.2.2:
|
||||
resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
eslint-visitor-keys@3.4.3:
|
||||
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
|
||||
@@ -3053,20 +3027,20 @@ packages:
|
||||
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
eslint@9.39.2:
|
||||
resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
eslint@8.57.1:
|
||||
resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
jiti: '*'
|
||||
peerDependenciesMeta:
|
||||
jiti:
|
||||
optional: true
|
||||
|
||||
espree@10.4.0:
|
||||
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
espree@9.6.1:
|
||||
resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
esquery@1.7.0:
|
||||
resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
|
||||
engines: {node: '>=0.10'}
|
||||
@@ -3138,9 +3112,9 @@ packages:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
file-entry-cache@8.0.0:
|
||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
file-entry-cache@6.0.1:
|
||||
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
||||
engines: {node: ^10.12.0 || >=12.0.0}
|
||||
|
||||
fill-range@7.1.1:
|
||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
@@ -3150,9 +3124,9 @@ packages:
|
||||
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
flat-cache@4.0.1:
|
||||
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
|
||||
engines: {node: '>=16'}
|
||||
flat-cache@3.2.0:
|
||||
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
|
||||
engines: {node: ^10.12.0 || >=12.0.0}
|
||||
|
||||
flatted@3.3.3:
|
||||
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
||||
@@ -3259,23 +3233,26 @@ packages:
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
hasBin: true
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
|
||||
glob@9.3.5:
|
||||
resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
|
||||
global-directory@4.0.1:
|
||||
resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
globals@13.24.0:
|
||||
resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
globals@14.0.0:
|
||||
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
globals@16.4.0:
|
||||
resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
globalthis@1.0.4:
|
||||
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3287,6 +3264,9 @@ packages:
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
graphemer@1.4.0:
|
||||
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
||||
|
||||
has-bigints@1.1.0:
|
||||
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3317,12 +3297,6 @@ packages:
|
||||
help-me@5.0.0:
|
||||
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
||||
|
||||
hermes-estree@0.25.1:
|
||||
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
|
||||
|
||||
hermes-parser@0.25.1:
|
||||
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
|
||||
|
||||
hoist-non-react-statics@3.3.2:
|
||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||
|
||||
@@ -3379,6 +3353,13 @@ packages:
|
||||
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
inflight@1.0.6:
|
||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
ini@4.1.1:
|
||||
resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
@@ -3479,6 +3460,10 @@ packages:
|
||||
resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-path-inside@3.0.3:
|
||||
resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-plain-obj@4.1.0:
|
||||
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -4024,6 +4009,10 @@ packages:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
path-is-absolute@1.0.1:
|
||||
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
path-key@3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4244,6 +4233,11 @@ packages:
|
||||
rfdc@1.4.1:
|
||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||
|
||||
rimraf@3.0.2:
|
||||
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||
hasBin: true
|
||||
|
||||
rollup@3.29.5:
|
||||
resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==}
|
||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||
@@ -4528,6 +4522,9 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
text-table@0.2.0:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
|
||||
thread-stream@4.0.0:
|
||||
resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -4585,6 +4582,10 @@ packages:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
type-fest@0.20.2:
|
||||
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
type-fest@0.7.1:
|
||||
resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4865,12 +4866,6 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
zod-validation-error@4.0.2:
|
||||
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.0 || ^4.0.0
|
||||
|
||||
zod@3.25.76:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
@@ -5254,29 +5249,27 @@ snapshots:
|
||||
'@esbuild/win32-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))':
|
||||
'@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)':
|
||||
dependencies:
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint: 8.57.1
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/regexpp@4.12.2': {}
|
||||
|
||||
'@eslint/config-array@0.21.1':
|
||||
'@eslint/eslintrc@2.1.4':
|
||||
dependencies:
|
||||
'@eslint/object-schema': 2.1.7
|
||||
ajv: 6.12.6
|
||||
debug: 4.4.3
|
||||
espree: 9.6.1
|
||||
globals: 13.24.0
|
||||
ignore: 5.3.2
|
||||
import-fresh: 3.3.1
|
||||
js-yaml: 4.1.1
|
||||
minimatch: 3.1.2
|
||||
strip-json-comments: 3.1.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@eslint/config-helpers@0.4.2':
|
||||
dependencies:
|
||||
'@eslint/core': 0.17.0
|
||||
|
||||
'@eslint/core@0.17.0':
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
|
||||
'@eslint/eslintrc@3.3.3':
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
@@ -5291,15 +5284,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@eslint/js@8.57.1': {}
|
||||
|
||||
'@eslint/js@9.39.2': {}
|
||||
|
||||
'@eslint/object-schema@2.1.7': {}
|
||||
|
||||
'@eslint/plugin-kit@0.4.1':
|
||||
dependencies:
|
||||
'@eslint/core': 0.17.0
|
||||
levn: 0.4.1
|
||||
|
||||
'@exodus/bytes@1.11.0': {}
|
||||
|
||||
'@formatjs/ecma402-abstract@3.1.1':
|
||||
@@ -5333,16 +5321,17 @@ snapshots:
|
||||
'@formatjs/fast-memoize': 3.1.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.7':
|
||||
'@humanwhocodes/config-array@0.13.0':
|
||||
dependencies:
|
||||
'@humanfs/core': 0.19.1
|
||||
'@humanwhocodes/retry': 0.4.3
|
||||
'@humanwhocodes/object-schema': 2.0.3
|
||||
debug: 4.4.3
|
||||
minimatch: 3.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@humanwhocodes/module-importer@1.0.1': {}
|
||||
|
||||
'@humanwhocodes/retry@0.4.3': {}
|
||||
'@humanwhocodes/object-schema@2.0.3': {}
|
||||
|
||||
'@img/colour@1.0.0':
|
||||
optional: true
|
||||
@@ -5556,12 +5545,12 @@ snapshots:
|
||||
fs-extra: 11.3.3
|
||||
prompts: 2.4.2
|
||||
|
||||
'@mintel/eslint-config@1.1.13(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
'@mintel/eslint-config@1.1.13(eslint@8.57.1)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint/eslintrc': 3.3.3
|
||||
'@eslint/js': 9.39.2
|
||||
eslint-config-next: 15.1.6(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
typescript-eslint: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint-config-next: 15.1.6(eslint@8.57.1)(typescript@5.9.3)
|
||||
typescript-eslint: 8.54.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- eslint
|
||||
- eslint-import-resolver-webpack
|
||||
@@ -5625,10 +5614,6 @@ snapshots:
|
||||
dependencies:
|
||||
fast-glob: 3.3.1
|
||||
|
||||
'@next/eslint-plugin-next@16.1.6':
|
||||
dependencies:
|
||||
fast-glob: 3.3.1
|
||||
|
||||
'@next/swc-darwin-arm64@15.1.6':
|
||||
optional: true
|
||||
|
||||
@@ -7060,7 +7045,7 @@ snapshots:
|
||||
|
||||
'@types/pg-pool@2.0.6':
|
||||
dependencies:
|
||||
'@types/pg': 8.6.1
|
||||
'@types/pg': 8.15.6
|
||||
|
||||
'@types/pg-pool@2.0.7':
|
||||
dependencies:
|
||||
@@ -7092,15 +7077,15 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
'@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.54.0
|
||||
'@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/type-utils': 8.54.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.54.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.54.0
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint: 8.57.1
|
||||
ignore: 7.0.5
|
||||
natural-compare: 1.4.0
|
||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||
@@ -7108,14 +7093,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
'@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.54.0
|
||||
'@typescript-eslint/types': 8.54.0
|
||||
'@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.54.0
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint: 8.57.1
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -7138,13 +7123,13 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
'@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
'@typescript-eslint/type-utils@8.54.0(eslint@8.57.1)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.54.0
|
||||
'@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.54.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint: 8.57.1
|
||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
@@ -7167,13 +7152,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
'@typescript-eslint/utils@8.54.0(eslint@8.57.1)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1)
|
||||
'@typescript-eslint/scope-manager': 8.54.0
|
||||
'@typescript-eslint/types': 8.54.0
|
||||
'@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint: 8.57.1
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -7183,6 +7168,8 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.54.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
||||
optional: true
|
||||
|
||||
@@ -7796,6 +7783,10 @@ snapshots:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
|
||||
doctrine@3.0.0:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
|
||||
dom-accessibility-api@0.5.16: {}
|
||||
|
||||
dom-accessibility-api@0.6.3: {}
|
||||
@@ -7984,19 +7975,19 @@ snapshots:
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
|
||||
eslint-config-next@15.1.6(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
||||
eslint-config-next@15.1.6(eslint@8.57.1)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 15.1.6
|
||||
'@rushstack/eslint-patch': 1.15.0
|
||||
'@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
'@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
@@ -8004,26 +7995,6 @@ snapshots:
|
||||
- eslint-plugin-import-x
|
||||
- supports-color
|
||||
|
||||
eslint-config-next@16.1.6(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 16.1.6
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1))
|
||||
globals: 16.4.0
|
||||
typescript-eslint: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- '@typescript-eslint/parser'
|
||||
- eslint-import-resolver-webpack
|
||||
- eslint-plugin-import-x
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-node@0.3.9:
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
@@ -8032,33 +8003,33 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)):
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint: 8.57.1
|
||||
get-tsconfig: 4.13.1
|
||||
is-bun-module: 2.0.0
|
||||
stable-hash: 0.0.5
|
||||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
'@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
@@ -8067,9 +8038,9 @@ snapshots:
|
||||
array.prototype.flatmap: 1.3.3
|
||||
debug: 3.2.7
|
||||
doctrine: 2.1.0
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
@@ -8081,13 +8052,13 @@ snapshots:
|
||||
string.prototype.trimend: 1.0.9
|
||||
tsconfig-paths: 3.15.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.6.1)):
|
||||
eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1):
|
||||
dependencies:
|
||||
aria-query: 5.3.2
|
||||
array-includes: 3.1.9
|
||||
@@ -8097,7 +8068,7 @@ snapshots:
|
||||
axobject-query: 4.1.0
|
||||
damerau-levenshtein: 1.0.8
|
||||
emoji-regex: 9.2.2
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint: 8.57.1
|
||||
hasown: 2.0.2
|
||||
jsx-ast-utils: 3.3.5
|
||||
language-tags: 1.0.9
|
||||
@@ -8106,22 +8077,11 @@ snapshots:
|
||||
safe-regex-test: 1.1.0
|
||||
string.prototype.includes: 2.0.1
|
||||
|
||||
eslint-plugin-react-hooks@5.2.0(eslint@9.39.2(jiti@2.6.1)):
|
||||
eslint-plugin-react-hooks@5.2.0(eslint@8.57.1):
|
||||
dependencies:
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint: 8.57.1
|
||||
|
||||
eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/parser': 7.29.0
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
hermes-parser: 0.25.1
|
||||
zod: 4.3.6
|
||||
zod-validation-error: 4.0.2(zod@4.3.6)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)):
|
||||
eslint-plugin-react@7.37.5(eslint@8.57.1):
|
||||
dependencies:
|
||||
array-includes: 3.1.9
|
||||
array.prototype.findlast: 1.2.5
|
||||
@@ -8129,7 +8089,7 @@ snapshots:
|
||||
array.prototype.tosorted: 1.1.4
|
||||
doctrine: 2.1.0
|
||||
es-iterator-helpers: 1.2.2
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint: 8.57.1
|
||||
estraverse: 5.3.0
|
||||
hasown: 2.0.2
|
||||
jsx-ast-utils: 3.3.5
|
||||
@@ -8148,7 +8108,7 @@ snapshots:
|
||||
esrecurse: 4.3.0
|
||||
estraverse: 4.3.0
|
||||
|
||||
eslint-scope@8.4.0:
|
||||
eslint-scope@7.2.2:
|
||||
dependencies:
|
||||
esrecurse: 4.3.0
|
||||
estraverse: 5.3.0
|
||||
@@ -8157,44 +8117,46 @@ snapshots:
|
||||
|
||||
eslint-visitor-keys@4.2.1: {}
|
||||
|
||||
eslint@9.39.2(jiti@2.6.1):
|
||||
eslint@8.57.1:
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1)
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@eslint/config-array': 0.21.1
|
||||
'@eslint/config-helpers': 0.4.2
|
||||
'@eslint/core': 0.17.0
|
||||
'@eslint/eslintrc': 3.3.3
|
||||
'@eslint/js': 9.39.2
|
||||
'@eslint/plugin-kit': 0.4.1
|
||||
'@humanfs/node': 0.16.7
|
||||
'@eslint/eslintrc': 2.1.4
|
||||
'@eslint/js': 8.57.1
|
||||
'@humanwhocodes/config-array': 0.13.0
|
||||
'@humanwhocodes/module-importer': 1.0.1
|
||||
'@humanwhocodes/retry': 0.4.3
|
||||
'@types/estree': 1.0.8
|
||||
'@nodelib/fs.walk': 1.2.8
|
||||
'@ungap/structured-clone': 1.3.0
|
||||
ajv: 6.12.6
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.3
|
||||
doctrine: 3.0.0
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 8.4.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
espree: 10.4.0
|
||||
eslint-scope: 7.2.2
|
||||
eslint-visitor-keys: 3.4.3
|
||||
espree: 9.6.1
|
||||
esquery: 1.7.0
|
||||
esutils: 2.0.3
|
||||
fast-deep-equal: 3.1.3
|
||||
file-entry-cache: 8.0.0
|
||||
file-entry-cache: 6.0.1
|
||||
find-up: 5.0.0
|
||||
glob-parent: 6.0.2
|
||||
globals: 13.24.0
|
||||
graphemer: 1.4.0
|
||||
ignore: 5.3.2
|
||||
imurmurhash: 0.1.4
|
||||
is-glob: 4.0.3
|
||||
is-path-inside: 3.0.3
|
||||
js-yaml: 4.1.1
|
||||
json-stable-stringify-without-jsonify: 1.0.1
|
||||
levn: 0.4.1
|
||||
lodash.merge: 4.6.2
|
||||
minimatch: 3.1.2
|
||||
natural-compare: 1.4.0
|
||||
optionator: 0.9.4
|
||||
optionalDependencies:
|
||||
jiti: 2.6.1
|
||||
strip-ansi: 6.0.1
|
||||
text-table: 0.2.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -8204,6 +8166,12 @@ snapshots:
|
||||
acorn-jsx: 5.3.2(acorn@8.15.0)
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
espree@9.6.1:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
acorn-jsx: 5.3.2(acorn@8.15.0)
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
esquery@1.7.0:
|
||||
dependencies:
|
||||
estraverse: 5.3.0
|
||||
@@ -8258,9 +8226,9 @@ snapshots:
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
|
||||
file-entry-cache@8.0.0:
|
||||
file-entry-cache@6.0.1:
|
||||
dependencies:
|
||||
flat-cache: 4.0.1
|
||||
flat-cache: 3.2.0
|
||||
|
||||
fill-range@7.1.1:
|
||||
dependencies:
|
||||
@@ -8271,10 +8239,11 @@ snapshots:
|
||||
locate-path: 6.0.0
|
||||
path-exists: 4.0.0
|
||||
|
||||
flat-cache@4.0.1:
|
||||
flat-cache@3.2.0:
|
||||
dependencies:
|
||||
flatted: 3.3.3
|
||||
keyv: 4.5.4
|
||||
rimraf: 3.0.2
|
||||
|
||||
flatted@3.3.3: {}
|
||||
|
||||
@@ -8385,6 +8354,15 @@ snapshots:
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
glob@7.2.3:
|
||||
dependencies:
|
||||
fs.realpath: 1.0.0
|
||||
inflight: 1.0.6
|
||||
inherits: 2.0.4
|
||||
minimatch: 3.1.2
|
||||
once: 1.4.0
|
||||
path-is-absolute: 1.0.1
|
||||
|
||||
glob@9.3.5:
|
||||
dependencies:
|
||||
fs.realpath: 1.0.0
|
||||
@@ -8396,9 +8374,11 @@ snapshots:
|
||||
dependencies:
|
||||
ini: 4.1.1
|
||||
|
||||
globals@14.0.0: {}
|
||||
globals@13.24.0:
|
||||
dependencies:
|
||||
type-fest: 0.20.2
|
||||
|
||||
globals@16.4.0: {}
|
||||
globals@14.0.0: {}
|
||||
|
||||
globalthis@1.0.4:
|
||||
dependencies:
|
||||
@@ -8409,6 +8389,8 @@ snapshots:
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
graphemer@1.4.0: {}
|
||||
|
||||
has-bigints@1.1.0: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
@@ -8433,12 +8415,6 @@ snapshots:
|
||||
|
||||
help-me@5.0.0: {}
|
||||
|
||||
hermes-estree@0.25.1: {}
|
||||
|
||||
hermes-parser@0.25.1:
|
||||
dependencies:
|
||||
hermes-estree: 0.25.1
|
||||
|
||||
hoist-non-react-statics@3.3.2:
|
||||
dependencies:
|
||||
react-is: 16.13.1
|
||||
@@ -8505,6 +8481,13 @@ snapshots:
|
||||
|
||||
indent-string@4.0.0: {}
|
||||
|
||||
inflight@1.0.6:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
wrappy: 1.0.2
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
ini@4.1.1: {}
|
||||
|
||||
internal-slot@1.1.0:
|
||||
@@ -8610,6 +8593,8 @@ snapshots:
|
||||
|
||||
is-obj@2.0.0: {}
|
||||
|
||||
is-path-inside@3.0.3: {}
|
||||
|
||||
is-plain-obj@4.1.0: {}
|
||||
|
||||
is-potential-custom-element-name@1.0.1: {}
|
||||
@@ -9154,6 +9139,8 @@ snapshots:
|
||||
|
||||
path-exists@4.0.0: {}
|
||||
|
||||
path-is-absolute@1.0.1: {}
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
path-parse@1.0.7: {}
|
||||
@@ -9381,6 +9368,10 @@ snapshots:
|
||||
|
||||
rfdc@1.4.1: {}
|
||||
|
||||
rimraf@3.0.2:
|
||||
dependencies:
|
||||
glob: 7.2.3
|
||||
|
||||
rollup@3.29.5:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
@@ -9763,6 +9754,8 @@ snapshots:
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
|
||||
text-table@0.2.0: {}
|
||||
|
||||
thread-stream@4.0.0:
|
||||
dependencies:
|
||||
real-require: 0.2.0
|
||||
@@ -9815,6 +9808,8 @@ snapshots:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
||||
type-fest@0.20.2: {}
|
||||
|
||||
type-fest@0.7.1: {}
|
||||
|
||||
typed-array-buffer@1.0.3:
|
||||
@@ -9850,13 +9845,13 @@ snapshots:
|
||||
possible-typed-array-names: 1.1.0
|
||||
reflect.getprototypeof: 1.0.10
|
||||
|
||||
typescript-eslint@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
||||
typescript-eslint@8.54.0(eslint@8.57.1)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
'@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
'@typescript-eslint/utils': 8.54.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -10143,10 +10138,6 @@ snapshots:
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zod-validation-error@4.0.2(zod@4.3.6):
|
||||
dependencies:
|
||||
zod: 4.3.6
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
||||
zod@4.3.6: {}
|
||||
|
||||
7
pnpm-workspace.yaml
Normal file
7
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
onlyBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- '@sentry/cli'
|
||||
- '@swc/core'
|
||||
- esbuild
|
||||
- sharp
|
||||
- unrs-resolver
|
||||
@@ -7,26 +7,36 @@ import { createCollection, createField, updateSettings } from "@directus/sdk";
|
||||
const client = createMintelDirectusClient();
|
||||
|
||||
async function setupBranding() {
|
||||
const prjName = process.env.PROJECT_NAME || "Mintel Project";
|
||||
const prjName = process.env.PROJECT_NAME || "MB Grid Solutions";
|
||||
const prjColor = process.env.PROJECT_COLOR || "#82ed20";
|
||||
|
||||
console.log(`🎨 Setup Directus Branding for ${prjName}...`);
|
||||
console.log(`🎨 Refining Directus Branding for ${prjName}...`);
|
||||
await ensureDirectusAuthenticated(client);
|
||||
|
||||
const cssInjection = `
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
body, .v-app { font-family: 'Inter', sans-serif !important; }
|
||||
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap');
|
||||
|
||||
body, .v-app { font-family: 'Outfit', sans-serif !important; }
|
||||
|
||||
.public-view .v-card {
|
||||
backdrop-filter: blur(20px);
|
||||
background: rgba(255, 255, 255, 0.9) !important;
|
||||
border-radius: 32px !important;
|
||||
box-shadow: 0 50px 100px -20px rgba(0, 0, 0, 0.4) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
.v-navigation-drawer { background: #000c24 !important; }
|
||||
|
||||
.v-list-item--active {
|
||||
color: ${prjColor} !important;
|
||||
background: rgba(130, 237, 32, 0.1) !important;
|
||||
}
|
||||
</style>
|
||||
<div style="font-family: 'Inter', sans-serif; text-align: center; margin-top: 24px;">
|
||||
<p style="color: rgba(255,255,255,0.7); font-size: 14px; margin-bottom: 4px; font-weight: 500;">MINTEL INFRASTRUCTURE ENGINE</p>
|
||||
<h1 style="color: #ffffff; font-size: 18px; font-weight: 700; margin: 0;">${prjName.toUpperCase()} <span style="color: ${prjColor};">RELIABILITY.</span></h1>
|
||||
<div style="font-family: 'Outfit', sans-serif; text-align: center; margin-top: 24px;">
|
||||
<p style="color: rgba(255,255,255,0.6); font-size: 11px; letter-spacing: 2px; margin-bottom: 4px; font-weight: 600; text-transform: uppercase;">Mintel Infrastructure Engine</p>
|
||||
<h1 style="color: #ffffff; font-size: 20px; font-weight: 700; margin: 0; letter-spacing: -0.5px;">${prjName.toUpperCase()} <span style="color: ${prjColor};">SYNC.</span></h1>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -36,24 +46,23 @@ async function setupBranding() {
|
||||
project_name: prjName,
|
||||
project_color: prjColor,
|
||||
public_note: cssInjection,
|
||||
module_bar_background: "#00081a",
|
||||
theme_light_overrides: {
|
||||
primary: prjColor,
|
||||
borderRadius: "16px",
|
||||
borderRadius: "12px",
|
||||
navigationBackground: "#000c24",
|
||||
navigationForeground: "#ffffff",
|
||||
moduleBarBackground: "#00081a",
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any),
|
||||
);
|
||||
console.log("✨ Branding applied!");
|
||||
|
||||
try {
|
||||
await createCollectionAndFields();
|
||||
console.log("🏗️ Schema alignment complete!");
|
||||
} catch (error) {
|
||||
console.error("❌ Error aligning schema:", error);
|
||||
}
|
||||
await createCollectionAndFields();
|
||||
console.log("🏗️ Schema alignment complete!");
|
||||
} catch (error) {
|
||||
console.error("❌ Error setting up branding:", error);
|
||||
console.error("❌ Error during bootstrap:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +77,11 @@ async function createCollectionAndFields() {
|
||||
meta: {
|
||||
icon: "contact_mail",
|
||||
display_template: "{{name}} <{{email}}>",
|
||||
group: null,
|
||||
sort: null,
|
||||
collapse: "open",
|
||||
},
|
||||
} as any),
|
||||
}),
|
||||
);
|
||||
|
||||
// Add ID field
|
||||
@@ -82,27 +94,57 @@ async function createCollectionAndFields() {
|
||||
}),
|
||||
);
|
||||
console.log(`✅ Collection ${collectionName} created.`);
|
||||
} catch (e: any) {
|
||||
console.log(`ℹ️ Collection ${collectionName} status: ${e.message}`);
|
||||
} catch {
|
||||
console.log(`ℹ️ Collection ${collectionName} exists.`);
|
||||
}
|
||||
|
||||
const safeAddField = async (field: string, type: string, meta: any = {}) => {
|
||||
const safeAddField = async (
|
||||
field: string,
|
||||
type: string,
|
||||
meta: Record<string, unknown> = {},
|
||||
) => {
|
||||
try {
|
||||
await client.request(createField(collectionName, { field, type, meta }));
|
||||
console.log(`✅ Field ${field} added.`);
|
||||
} catch (e: any) {
|
||||
} catch {
|
||||
// Ignore if exists
|
||||
}
|
||||
};
|
||||
|
||||
await safeAddField("name", "string", { interface: "input" });
|
||||
await safeAddField("email", "string", { interface: "input" });
|
||||
await safeAddField("company", "string", { interface: "input" });
|
||||
await safeAddField("message", "text", { interface: "textarea" });
|
||||
await safeAddField("name", "string", {
|
||||
interface: "input",
|
||||
display: "raw",
|
||||
width: "half",
|
||||
});
|
||||
await safeAddField("email", "string", {
|
||||
interface: "input",
|
||||
display: "raw",
|
||||
width: "half",
|
||||
});
|
||||
await safeAddField("company", "string", {
|
||||
interface: "input",
|
||||
display: "raw",
|
||||
width: "half",
|
||||
});
|
||||
await safeAddField("message", "text", {
|
||||
interface: "textarea",
|
||||
display: "raw",
|
||||
width: "full",
|
||||
});
|
||||
await safeAddField("date_created", "timestamp", {
|
||||
interface: "datetime",
|
||||
special: ["date-created"],
|
||||
display: "datetime",
|
||||
display_options: { relative: true },
|
||||
width: "half",
|
||||
});
|
||||
}
|
||||
|
||||
setupBranding();
|
||||
setupBranding()
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("🚨 Fatal bootstrap error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Mintel Directus Sync Engine
|
||||
# Synchronizes Directus Data (Postgres + Uploads) between Local and Remote
|
||||
|
||||
# Configuration
|
||||
REMOTE_HOST="${SSH_HOST:-root@alpha.mintel.me}"
|
||||
ACTION=$1
|
||||
ENV=$2
|
||||
|
||||
# Help
|
||||
if [ -z "$ACTION" ] || [ -z "$ENV" ]; then
|
||||
echo "Usage: mintel-sync [push|pull] [testing|staging|production]"
|
||||
echo "Usage: ./scripts/sync-directus.sh [push|pull] [testing|staging|production]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " push Sync LOCAL data -> REMOTE"
|
||||
@@ -20,7 +18,10 @@ if [ -z "$ACTION" ] || [ -z "$ENV" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PRJ_ID=$(jq -r .name package.json | sed 's/@mintel\///')
|
||||
# Project Configuration (extracted from package.json and aligned with deploy.yml)
|
||||
PRJ_ID=$(jq -r .name package.json | sed 's/@mintel\///' | sed 's/\.com$//')
|
||||
REMOTE_DIR="/home/deploy/sites/${PRJ_ID}.com"
|
||||
|
||||
case $ENV in
|
||||
testing) PROJECT_NAME="${PRJ_ID}-testing"; ENV_FILE=".env.testing" ;;
|
||||
staging) PROJECT_NAME="${PRJ_ID}-staging"; ENV_FILE=".env.staging" ;;
|
||||
@@ -28,41 +29,90 @@ case $ENV in
|
||||
*) echo "❌ Invalid environment: $ENV"; exit 1 ;;
|
||||
esac
|
||||
|
||||
REMOTE_DIR="/home/deploy/sites/${PRJ_ID}.com"
|
||||
|
||||
# DB Details
|
||||
# DB Details (matching docker-compose defaults)
|
||||
DB_USER="directus"
|
||||
DB_NAME="directus"
|
||||
|
||||
echo "🔍 Detecting local database..."
|
||||
LOCAL_DB_CONTAINER=$(docker compose ps -q directus-db)
|
||||
if [ -z "$LOCAL_DB_CONTAINER" ]; then
|
||||
echo "❌ Local directus-db container not found. Running?"
|
||||
echo "❌ Local directus-db container not found. Is it running? (npm run dev)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$ACTION" == "push" ]; then
|
||||
echo "🚀 Pushing LOCAL -> $ENV ($PROJECT_NAME)..."
|
||||
|
||||
# 1. DB Dump
|
||||
echo "📦 Dumping local database..."
|
||||
docker exec "$LOCAL_DB_CONTAINER" pg_dump -U "$DB_USER" --clean --if-exists --no-owner --no-privileges "$DB_NAME" > dump.sql
|
||||
|
||||
# 2. Upload Dump
|
||||
echo "📤 Uploading dump to remote server..."
|
||||
scp dump.sql "$REMOTE_HOST:$REMOTE_DIR/dump.sql"
|
||||
|
||||
# 3. Restore on Remote
|
||||
echo "🔄 Restoring dump on $ENV..."
|
||||
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus-db")
|
||||
|
||||
if [ -z "$REMOTE_DB_CONTAINER" ]; then
|
||||
echo "❌ Remote $ENV-db container not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🧹 Wiping remote database schema..."
|
||||
ssh "$REMOTE_HOST" "docker exec $REMOTE_DB_CONTAINER psql -U $DB_USER $DB_NAME -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'"
|
||||
|
||||
echo "⚡ Restoring database..."
|
||||
ssh "$REMOTE_HOST" "docker exec -i $REMOTE_DB_CONTAINER psql -U $DB_USER $DB_NAME < $REMOTE_DIR/dump.sql"
|
||||
|
||||
# 4. Sync Uploads
|
||||
echo "📁 Syncing uploads (Local -> $ENV)..."
|
||||
rsync -avz --progress ./directus/uploads/ "$REMOTE_HOST:$REMOTE_DIR/directus/uploads/"
|
||||
|
||||
# Clean up
|
||||
rm dump.sql
|
||||
ssh "$REMOTE_HOST" "rm $REMOTE_DIR/dump.sql"
|
||||
echo "✨ Push complete!"
|
||||
|
||||
# 5. Restart Directus to trigger migrations and refresh schema cache
|
||||
echo "🔄 Restarting remote Directus to apply migrations..."
|
||||
ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME restart directus"
|
||||
|
||||
echo "✨ Push to $ENV complete!"
|
||||
|
||||
elif [ "$ACTION" == "pull" ]; then
|
||||
echo "📥 Pulling $ENV -> LOCAL..."
|
||||
echo "📥 Pulling $ENV Data -> LOCAL..."
|
||||
|
||||
# 1. DB Dump on Remote
|
||||
echo "📦 Dumping remote database ($ENV)..."
|
||||
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus-db")
|
||||
|
||||
if [ -z "$REMOTE_DB_CONTAINER" ]; then
|
||||
echo "❌ Remote $ENV-db container not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ssh "$REMOTE_HOST" "docker exec $REMOTE_DB_CONTAINER pg_dump -U $DB_USER --clean --if-exists --no-owner --no-privileges $DB_NAME > $REMOTE_DIR/dump.sql"
|
||||
|
||||
# 2. Download Dump
|
||||
echo "📥 Downloading dump..."
|
||||
scp "$REMOTE_HOST:$REMOTE_DIR/dump.sql" dump.sql
|
||||
|
||||
# 3. Restore Locally
|
||||
echo "🧹 Wiping local database schema..."
|
||||
docker exec "$LOCAL_DB_CONTAINER" psql -U "$DB_USER" "$DB_NAME" -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'
|
||||
|
||||
echo "⚡ Restoring database locally..."
|
||||
docker exec -i "$LOCAL_DB_CONTAINER" psql -U "$DB_USER" "$DB_NAME" < dump.sql
|
||||
|
||||
# 4. Sync Uploads
|
||||
echo "📁 Syncing uploads ($ENV -> Local)..."
|
||||
rsync -avz --progress "$REMOTE_HOST:$REMOTE_DIR/directus/uploads/" ./directus/uploads/
|
||||
|
||||
# Clean up
|
||||
rm dump.sql
|
||||
ssh "$REMOTE_HOST" "rm $REMOTE_DIR/dump.sql"
|
||||
echo "✨ Pull complete!"
|
||||
|
||||
echo "✨ Pull to Local complete!"
|
||||
fi
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@ import { validateMintelEnv } from "@mintel/next-utils";
|
||||
try {
|
||||
validateMintelEnv();
|
||||
console.log("✅ Environment variables validated");
|
||||
} catch (error) {
|
||||
} catch {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user