chore(workspace): add gitea repository url to all packages
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Failing after 18s
Build & Deploy / 🏗️ Build (push) Failing after 25s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s

This commit is contained in:
2026-02-27 11:27:21 +01:00
parent 4a5017124a
commit 96de68a063
12 changed files with 8045 additions and 13907 deletions

View File

@@ -235,6 +235,14 @@ jobs:
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_COLOR: ${{ secrets.PROJECT_COLOR || vars.PROJECT_COLOR || '#ff00ff' }}
# S3 Object Storage
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT || vars.S3_ENDPOINT || 'https://fsn1.your-objectstorage.com' }}
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY || vars.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY || vars.S3_SECRET_KEY }}
S3_BUCKET: ${{ secrets.S3_BUCKET || vars.S3_BUCKET || 'mintel' }}
S3_REGION: ${{ secrets.S3_REGION || vars.S3_REGION || 'fsn1' }}
S3_PREFIX: ${{ secrets.S3_PREFIX || vars.S3_PREFIX || github.event.repository.name }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -297,6 +305,14 @@ jobs:
NEXT_PUBLIC_UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID
UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
# S3 Object Storage
S3_ENDPOINT=$S3_ENDPOINT
S3_ACCESS_KEY=$S3_ACCESS_KEY
S3_SECRET_KEY=$S3_SECRET_KEY
S3_BUCKET=$S3_BUCKET
S3_REGION=$S3_REGION
S3_PREFIX=$S3_PREFIX
TARGET=$TARGET
SENTRY_ENVIRONMENT=$TARGET
PROJECT_NAME=$PROJECT_NAME

4
.gitignore vendored
View File

@@ -47,3 +47,7 @@ pnpm-debug.log*
.cache/
cloned-websites/
storage/
# Estimation Engine Data
data/crawls/
apps/web/out/estimations/

View File

@@ -48,30 +48,10 @@ export const technologies: Record<string, TechInfo> = {
'Using TypeScript means your application is robust and reliable from day one. It dramatically reduces the risk of "runtime errors" that could crash your site, saving time and money on bug fixes down the line.',
color: "bg-blue-600 text-white",
related: [
{ name: "Directus CMS", slug: "directus-cms" },
{ name: "Next.js 14", slug: "next-js-14" },
],
},
"directus-cms": {
title: "Directus CMS",
subtitle: "The Open Data Platform",
description:
"Directus is a modern, headless Content Management System (CMS) that instantly turns any database into a beautiful, easy-to-use application for managing your content. Unlike traditional CMSs, it doesn't dictate how your website looks.",
icon: Database,
benefits: [
"Intuitive interface for non-technical editors",
"Complete freedom regarding front-end design",
"Real-time updates and live previews",
"Highly secure and role-based access control",
],
customerValue:
"Directus gives you full control over your content without needing a developer for every text change. It separates your data from the design, ensuring your website can evolve visually without rebuilding your entire content library.",
color: "bg-purple-600 text-white",
related: [
{ name: "Next.js 14", slug: "next-js-14" },
{ name: "Tailwind CSS", slug: "tailwind-css" },
],
},
"tailwind-css": {
title: "Tailwind CSS",
subtitle: "Utility-First CSS Framework",

View File

@@ -10,6 +10,18 @@ const dirname = path.dirname(filename);
/** @type {import('next').NextConfig} */
const nextConfig = {
serverExternalPackages: ['@mintel/content-engine'],
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '*.your-objectstorage.com',
},
{
protocol: 'https',
hostname: 'fsn1.your-objectstorage.com',
},
],
},
async rewrites() {
return [
// Umami proxy rewrite handled in app/stats/api/send/route.ts

View File

@@ -5,7 +5,7 @@
"description": "Technical problem solver's blog - practical insights and learning notes",
"scripts": {
"dev": "pnpm run seed:context && next dev --turbo",
"dev:native": "pnpm run seed:context && DATABASE_URI=postgres://directus:directus@127.0.0.1:54321/directus PAYLOAD_SECRET=dev-secret next dev --webpack",
"dev:native": "pnpm run seed:context && DATABASE_URI=postgres://payload:payload@127.0.0.1:54321/payload PAYLOAD_SECRET=dev-secret next dev --webpack",
"seed:context": "tsx ./seed-context.ts",
"build": "next build --webpack",
"start": "next start",
@@ -24,13 +24,16 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.750.0",
"@emotion/is-prop-valid": "^1.4.0",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@mintel/cloner": "^1.8.0",
"@mintel/concept-engine": "link:../../../at-mintel/packages/concept-engine",
"@mintel/content-engine": "link:../../../at-mintel/packages/content-engine",
"@mintel/estimation-engine": "link:../../../at-mintel/packages/estimation-engine",
"@mintel/meme-generator": "link:../../../at-mintel/packages/meme-generator",
"@mintel/pdf": "^1.8.0",
"@mintel/pdf": "link:../../../at-mintel/packages/pdf-library",
"@mintel/thumbnail-generator": "link:../../../at-mintel/packages/thumbnail-generator",
"@next/mdx": "^16.1.6",
"@next/third-parties": "^16.1.6",
@@ -42,6 +45,7 @@
"@payloadcms/email-nodemailer": "^3.77.0",
"@payloadcms/next": "^3.77.0",
"@payloadcms/richtext-lexical": "^3.77.0",
"@payloadcms/storage-s3": "^3.77.0",
"@payloadcms/ui": "^3.77.0",
"@react-pdf/renderer": "^4.3.2",
"@remotion/bundler": "^4.0.414",
@@ -60,6 +64,7 @@
"canvas-confetti": "^1.9.4",
"clsx": "^2.1.1",
"crawlee": "^3.15.3",
"dotenv": "^17.3.1",
"esbuild": "^0.27.3",
"framer-motion": "^12.29.2",
"graphql": "^16.12.0",
@@ -102,6 +107,7 @@
"@mintel/tsconfig": "^1.7.3",
"@next/eslint-plugin-next": "^16.1.6",
"@tailwindcss/typography": "^0.5.15",
"@types/mime-types": "^3.0.1",
"@types/node": "^25.0.6",
"@types/nodemailer": "^7.0.10",
"@types/prismjs": "^1.26.5",
@@ -112,9 +118,14 @@
"eslint": "10.0.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"mime-types": "^3.0.2",
"postcss": "^8.4.49",
"tsx": "^4.21.0",
"typescript": "5.9.3",
"typescript-eslint": "^8.54.0"
},
"repository": {
"type": "git",
"url": "git@git.infra.mintel.me:mmintel/mintel.me.git"
}
}

View File

@@ -13,53 +13,53 @@
* via the `definition` "supportedTimezones".
*/
export type SupportedTimezones =
| "Pacific/Midway"
| "Pacific/Niue"
| "Pacific/Honolulu"
| "Pacific/Rarotonga"
| "America/Anchorage"
| "Pacific/Gambier"
| "America/Los_Angeles"
| "America/Tijuana"
| "America/Denver"
| "America/Phoenix"
| "America/Chicago"
| "America/Guatemala"
| "America/New_York"
| "America/Bogota"
| "America/Caracas"
| "America/Santiago"
| "America/Buenos_Aires"
| "America/Sao_Paulo"
| "Atlantic/South_Georgia"
| "Atlantic/Azores"
| "Atlantic/Cape_Verde"
| "Europe/London"
| "Europe/Berlin"
| "Africa/Lagos"
| "Europe/Athens"
| "Africa/Cairo"
| "Europe/Moscow"
| "Asia/Riyadh"
| "Asia/Dubai"
| "Asia/Baku"
| "Asia/Karachi"
| "Asia/Tashkent"
| "Asia/Calcutta"
| "Asia/Dhaka"
| "Asia/Almaty"
| "Asia/Jakarta"
| "Asia/Bangkok"
| "Asia/Shanghai"
| "Asia/Singapore"
| "Asia/Tokyo"
| "Asia/Seoul"
| "Australia/Brisbane"
| "Australia/Sydney"
| "Pacific/Guam"
| "Pacific/Noumea"
| "Pacific/Auckland"
| "Pacific/Fiji";
| 'Pacific/Midway'
| 'Pacific/Niue'
| 'Pacific/Honolulu'
| 'Pacific/Rarotonga'
| 'America/Anchorage'
| 'Pacific/Gambier'
| 'America/Los_Angeles'
| 'America/Tijuana'
| 'America/Denver'
| 'America/Phoenix'
| 'America/Chicago'
| 'America/Guatemala'
| 'America/New_York'
| 'America/Bogota'
| 'America/Caracas'
| 'America/Santiago'
| 'America/Buenos_Aires'
| 'America/Sao_Paulo'
| 'Atlantic/South_Georgia'
| 'Atlantic/Azores'
| 'Atlantic/Cape_Verde'
| 'Europe/London'
| 'Europe/Berlin'
| 'Africa/Lagos'
| 'Europe/Athens'
| 'Africa/Cairo'
| 'Europe/Moscow'
| 'Asia/Riyadh'
| 'Asia/Dubai'
| 'Asia/Baku'
| 'Asia/Karachi'
| 'Asia/Tashkent'
| 'Asia/Calcutta'
| 'Asia/Dhaka'
| 'Asia/Almaty'
| 'Asia/Jakarta'
| 'Asia/Bangkok'
| 'Asia/Shanghai'
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Brisbane'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
| 'Pacific/Auckland'
| 'Pacific/Fiji';
export interface Config {
auth: {
@@ -72,11 +72,11 @@ export interface Config {
posts: Post;
inquiries: Inquiry;
redirects: Redirect;
"context-files": ContextFile;
"payload-kv": PayloadKv;
"payload-locked-documents": PayloadLockedDocument;
"payload-preferences": PayloadPreference;
"payload-migrations": PayloadMigration;
'context-files': ContextFile;
'payload-kv': PayloadKv;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsSelect: {
@@ -85,27 +85,21 @@ export interface Config {
posts: PostsSelect<false> | PostsSelect<true>;
inquiries: InquiriesSelect<false> | InquiriesSelect<true>;
redirects: RedirectsSelect<false> | RedirectsSelect<true>;
"context-files": ContextFilesSelect<false> | ContextFilesSelect<true>;
"payload-kv": PayloadKvSelect<false> | PayloadKvSelect<true>;
"payload-locked-documents":
| PayloadLockedDocumentsSelect<false>
| PayloadLockedDocumentsSelect<true>;
"payload-preferences":
| PayloadPreferencesSelect<false>
| PayloadPreferencesSelect<true>;
"payload-migrations":
| PayloadMigrationsSelect<false>
| PayloadMigrationsSelect<true>;
'context-files': ContextFilesSelect<false> | ContextFilesSelect<true>;
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: number;
};
fallbackLocale: null;
globals: {
"ai-settings": AiSetting;
'ai-settings': AiSetting;
};
globalsSelect: {
"ai-settings": AiSettingsSelect<false> | AiSettingsSelect<true>;
'ai-settings': AiSettingsSelect<false> | AiSettingsSelect<true>;
};
locale: null;
user: User;
@@ -155,7 +149,7 @@ export interface User {
}[]
| null;
password?: string | null;
collection: "users";
collection: 'users';
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -234,8 +228,8 @@ export interface Post {
version: number;
[k: string]: unknown;
}[];
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
@@ -243,7 +237,7 @@ export interface Post {
} | null;
updatedAt: string;
createdAt: string;
_status?: ("draft" | "published") | null;
_status?: ('draft' | 'published') | null;
}
/**
* Contact form leads and inquiries.
@@ -333,32 +327,32 @@ export interface PayloadLockedDocument {
id: number;
document?:
| ({
relationTo: "users";
relationTo: 'users';
value: number | User;
} | null)
| ({
relationTo: "media";
relationTo: 'media';
value: number | Media;
} | null)
| ({
relationTo: "posts";
relationTo: 'posts';
value: number | Post;
} | null)
| ({
relationTo: "inquiries";
relationTo: 'inquiries';
value: number | Inquiry;
} | null)
| ({
relationTo: "redirects";
relationTo: 'redirects';
value: number | Redirect;
} | null)
| ({
relationTo: "context-files";
relationTo: 'context-files';
value: number | ContextFile;
} | null);
globalSlug?: string | null;
user: {
relationTo: "users";
relationTo: 'users';
value: number | User;
};
updatedAt: string;
@@ -371,7 +365,7 @@ export interface PayloadLockedDocument {
export interface PayloadPreference {
id: number;
user: {
relationTo: "users";
relationTo: 'users';
value: number | User;
};
key?: string | null;
@@ -609,6 +603,7 @@ export interface Auth {
[k: string]: unknown;
}
declare module "payload" {
declare module 'payload' {
export interface GeneratedTypes extends Config {}
}
}

View File

@@ -4,6 +4,7 @@ import { postgresAdapter } from "@payloadcms/db-postgres";
import { lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical";
import { payloadBlocks } from "./src/payload/blocks/allBlocks";
import { nodemailerAdapter } from "@payloadcms/email-nodemailer";
import { s3Storage } from "@payloadcms/storage-s3";
import path from "path";
import { fileURLToPath } from "url";
import sharp from "sharp";
@@ -31,19 +32,19 @@ export default buildConfig({
globals: [AiSettings],
...(process.env.MAIL_HOST
? {
email: nodemailerAdapter({
defaultFromAddress: process.env.MAIL_FROM || "info@mintel.me",
defaultFromName: "Mintel.me",
transportOptions: {
host: process.env.MAIL_HOST,
port: parseInt(process.env.MAIL_PORT || "587"),
auth: {
user: process.env.MAIL_USERNAME,
pass: process.env.MAIL_PASSWORD,
},
email: nodemailerAdapter({
defaultFromAddress: process.env.MAIL_FROM || "info@mintel.me",
defaultFromName: "Mintel.me",
transportOptions: {
host: process.env.MAIL_HOST,
port: parseInt(process.env.MAIL_PORT || "587"),
auth: {
user: process.env.MAIL_USERNAME,
pass: process.env.MAIL_PASSWORD,
},
}),
}
},
}),
}
: {}),
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
@@ -64,5 +65,27 @@ export default buildConfig({
},
}),
sharp,
plugins: [],
plugins: [
...(process.env.S3_ENDPOINT
? [
s3Storage({
collections: {
media: {
prefix: `${process.env.S3_PREFIX || "mintel-me"}/media`,
},
},
bucket: process.env.S3_BUCKET || "",
config: {
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY || "",
secretAccessKey: process.env.S3_SECRET_KEY || "",
},
region: process.env.S3_REGION || "fsn1",
endpoint: process.env.S3_ENDPOINT,
forcePathStyle: true,
},
}),
]
: []),
],
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,122 @@
import { getPayload } from "payload";
import configPromise from "../payload.config";
import fs from "fs";
import path from "path";
import { parseMarkdownToLexical } from "../src/payload/utils/lexicalParser";
function extractFrontmatter(content: string) {
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
if (!fmMatch) return {};
const fm = fmMatch[1];
const titleMatch = fm.match(/title:\s*"?([^"\n]+)"?/);
const descMatch = fm.match(/description:\s*"?([^"\n]+)"?/);
const tagsMatch = fm.match(/tags:\s*\[(.*?)\]/);
return {
title: titleMatch ? titleMatch[1] : "Untitled Draft",
description: descMatch ? descMatch[1] : "No description",
tags: tagsMatch ? tagsMatch[1].split(",").map(s => s.trim().replace(/"/g, "")) : []
};
}
async function run() {
try {
const payload = await getPayload({ config: configPromise });
console.log("Payload initialized.");
const draftsDir = path.resolve(process.cwd(), "content/drafts");
const publicBlogDir = path.resolve(process.cwd(), "public/blog");
if (!fs.existsSync(draftsDir)) {
console.log(`Drafts directory not found at ${draftsDir}`);
process.exit(0);
}
const files = fs.readdirSync(draftsDir).filter(f => f.endsWith(".md"));
let count = 0;
for (const file of files) {
console.log(`Processing ${file}...`);
const filePath = path.join(draftsDir, file);
const content = fs.readFileSync(filePath, "utf8");
const fm = extractFrontmatter(content);
const lexicalNodes = parseMarkdownToLexical(content);
const lexicalContent = {
root: {
type: "root",
format: "" as const,
indent: 0,
version: 1,
direction: "ltr" as const,
children: lexicalNodes
}
};
// Upload thumbnail if exists
let featuredImageId = null;
const thumbPath = path.join(publicBlogDir, `${file}.png`);
if (fs.existsSync(thumbPath)) {
console.log(`Uploading thumbnail ${file}.png...`);
const fileData = fs.readFileSync(thumbPath);
const stat = fs.statSync(thumbPath);
try {
const newMedia = await payload.create({
collection: "media",
data: {
alt: `Thumbnail for ${fm.title}`,
},
file: {
data: fileData,
name: `optimized-${file}.png`,
mimetype: "image/png",
size: stat.size,
},
});
featuredImageId = newMedia.id;
} catch (e) {
console.log("Failed to upload thumbnail", e);
}
}
const tagsArray = fm.tags.map(tag => ({ tag }));
const slug = fm.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "").substring(0, 60);
// Check if already exists
const existing = await payload.find({
collection: "posts",
where: { slug: { equals: slug } },
});
if (existing.totalDocs === 0) {
await payload.create({
collection: "posts",
data: {
title: fm.title,
slug: slug,
description: fm.description,
date: new Date().toISOString(),
tags: tagsArray,
featuredImage: featuredImageId,
content: lexicalContent,
_status: "published"
},
});
console.log(`Created CMS entry for ${file}.`);
count++;
} else {
console.log(`Post with slug ${slug} already exists. Skipping.`);
}
}
console.log(`Migration successful! Added ${count} new optimized posts to the database.`);
process.exit(0);
} catch (e) {
console.error("Migration failed:", e);
process.exit(1);
}
}
run();

View File

@@ -27,7 +27,7 @@ services:
- NEXT_TELEMETRY_DISABLED=1
- CI=true
- NPM_TOKEN=${NPM_TOKEN:-}
- DATABASE_URI=postgres://${postgres_DB_USER:-directus}:${postgres_DB_PASSWORD:-directus}@postgres-db:5432/${postgres_DB_NAME:-directus}
- DATABASE_URI=postgres://${postgres_DB_USER:-payload}:${postgres_DB_PASSWORD:-payload}@postgres-db:5432/${postgres_DB_NAME:-payload}
- PAYLOAD_SECRET=dev-secret
command: >
sh -c "pnpm install && pnpm --filter @mintel/web dev"
@@ -50,11 +50,11 @@ services:
env_file:
- .env
environment:
POSTGRES_DB: ${postgres_DB_NAME:-directus}
POSTGRES_USER: ${postgres_DB_USER:-directus}
POSTGRES_PASSWORD: ${postgres_DB_PASSWORD:-directus}
POSTGRES_DB: ${postgres_DB_NAME:-payload}
POSTGRES_USER: ${postgres_DB_USER:-payload}
POSTGRES_PASSWORD: ${postgres_DB_PASSWORD:-payload}
volumes:
- directus-db-data:/var/lib/postgresql/data
- payload-db-data:/var/lib/postgresql/data
ports:
- "54321:5432"
@@ -65,7 +65,7 @@ networks:
external: true
volumes:
directus-db-data:
payload-db-data:
node_modules:
apps_node_modules:
pnpm_store:

View File

@@ -87,11 +87,11 @@ services:
env_file:
- ${ENV_FILE:-.env}
environment:
POSTGRES_DB: ${postgres_DB_NAME:-directus}
POSTGRES_USER: ${postgres_DB_USER:-directus}
POSTGRES_PASSWORD: ${postgres_DB_PASSWORD:-directus}
POSTGRES_DB: ${postgres_DB_NAME:-payload}
POSTGRES_USER: ${postgres_DB_USER:-payload}
POSTGRES_PASSWORD: ${postgres_DB_PASSWORD:-payload}
volumes:
- directus-db-data:/var/lib/postgresql/data
- payload-db-data:/var/lib/postgresql/data
networks:
default:
@@ -101,6 +101,6 @@ networks:
external: true
volumes:
directus-db-data:
payload-db-data:
external: true
name: mintel-me_directus-db-data
name: mintel-me_payload-db-data

20288
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff