diff --git a/apps/web/app/about/page.tsx b/apps/web/app/about/page.tsx index ebfab3f..a10fabb 100644 --- a/apps/web/app/about/page.tsx +++ b/apps/web/app/about/page.tsx @@ -23,6 +23,7 @@ import { } from "../../src/components/Typography"; import { BackgroundGrid, Card, Container } from "../../src/components/Layout"; import { Button } from "../../src/components/Button"; +import { IconList, IconListItem } from "../../src/components/IconList"; export default function AboutPage() { return ( @@ -51,11 +52,10 @@ export default function AboutPage() {
Marc Mintel
@@ -116,18 +116,17 @@ export default function AboutPage() { Ergebnisse, nicht Prozesse. - + @@ -307,8 +306,8 @@ export default function AboutPage() {
- -
+ + {[ { label: "Direkte Kommunikation", @@ -323,21 +322,16 @@ export default function AboutPage() { desc: "Code, der hält, was er verspricht.", }, ].map((item, i) => ( - -
-
- -
-
-

{item.label}

- - {item.desc} - -
+ +
+

{item.label}

+ + {item.desc} +
- +
))} -
+
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 507482d..5a5e8a6 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -24,6 +24,7 @@ import { } from "../src/components/Typography"; import { BackgroundGrid, Card, Container } from "../src/components/Layout"; import { Button } from "../src/components/Button"; +import { IconList, IconListItem } from "../src/components/IconList"; export default function LandingPage() { return ( @@ -94,7 +95,7 @@ export default function LandingPage() {
- +
@@ -129,24 +131,25 @@ export default function LandingPage() {
- + diff --git a/apps/web/app/websites/page.tsx b/apps/web/app/websites/page.tsx index b81860e..4583e06 100644 --- a/apps/web/app/websites/page.tsx +++ b/apps/web/app/websites/page.tsx @@ -1,9 +1,9 @@ -'use client'; +"use client"; -import { PageHeader } from '../../src/components/PageHeader'; -import { Reveal } from '../../src/components/Reveal'; -import { Section } from '../../src/components/Section'; -import { +import { PageHeader } from "../../src/components/PageHeader"; +import { Reveal } from "../../src/components/Reveal"; +import { Section } from "../../src/components/Section"; +import { SystemArchitecture, SpeedPerformance, SolidFoundation, @@ -15,29 +15,41 @@ import { ConceptCommunication, ConceptPrototyping, ConceptSystem, - ConceptTarget -} from '../../src/components/Landing'; -import { Check } from 'lucide-react'; -import { H2, H3, H4, LeadText, BodyText, Label } from '../../src/components/Typography'; -import { BackgroundGrid, Card } from '../../src/components/Layout'; -import { MotionButton } from '../../src/components/Button'; + ConceptTarget, +} from "../../src/components/Landing"; +import { Check } from "lucide-react"; +import { + H2, + H3, + H4, + LeadText, + BodyText, + Label, +} from "../../src/components/Typography"; +import { BackgroundGrid, Card } from "../../src/components/Layout"; +import { MotionButton } from "../../src/components/Button"; +import { IconList, IconListItem } from "../../src/components/IconList"; export default function WebsitesPage() { return (
- - Websites, die
einfach funktionieren.} + + Websites, die
+ einfach funktionieren. + + } description="Keine Baukästen, keine Plugins, kein Overhead. Nur sauberer Code und maximale Performance." - backLink={{ href: '/', label: 'Zurück' }} + backLink={{ href: "/", label: "Zurück" }} backgroundSymbol="W" /> {/* Intro / Problem */} -
} @@ -51,17 +63,18 @@ export default function WebsitesPage() { - Eine Website ist kein Flyer. Sie ist ein Werkzeug, das jeden Tag arbeitet. - Deshalb baue ich sie stabil, schnell und wartungsfrei. + Eine Website ist kein Flyer. Sie ist ein{" "} + Werkzeug, das jeden Tag + arbeitet. Deshalb baue ich sie stabil, schnell und wartungsfrei.
{[ - { label: 'Stabil', icon: ConceptSystem }, - { label: 'Schnell', icon: ConceptAutomation }, - { label: 'Wartungsfrei', icon: ConceptCode }, - { label: 'Sicher', icon: ConceptTarget }, + { label: "Stabil", icon: ConceptSystem }, + { label: "Schnell", icon: ConceptAutomation }, + { label: "Wartungsfrei", icon: ConceptCode }, + { label: "Sicher", icon: ConceptTarget }, ].map((item, i) => (
@@ -76,8 +89,8 @@ export default function WebsitesPage() {
{/* Speed */} -

Geschwindigkeit ist
- kein Extra. Sie ist Standard. + + kein Extra. Sie ist Standard. +

- Viele Websites sind langsam, weil sie zusammengeklickt sind. Meine sind schnell, weil sie von Grund auf entwickelt wurden. + Viele Websites sind langsam, weil sie zusammengeklickt sind. + Meine sind schnell, weil sie{" "} + von Grund auf{" "} + entwickelt wurden. -
    + {[ - 'Seiten laden ohne Verzögerung', - 'Optimiert für Suchmaschinen (SEO)', - 'Bessere Nutzererfahrung', - 'Höhere Conversion-Rates', + "Seiten laden ohne Verzögerung", + "Optimiert für Suchmaschinen (SEO)", + "Bessere Nutzererfahrung", + "Höhere Conversion-Rates", ].map((item, i) => ( -
  • -
    + {item} -
  • + ))} -
+
- -
90+
+ +
+ 90+ +
@@ -126,8 +149,8 @@ export default function WebsitesPage() {
{/* No Maintenance */} -
} @@ -141,8 +164,9 @@ export default function WebsitesPage() { - Ich nutze keine Baukästen, die sich selbst zerstören. - Ihre Website besteht aus sauberem Code, der Ihnen gehört. + Ich nutze keine Baukästen, die sich selbst zerstören. Ihre Website + besteht aus sauberem Code, + der Ihnen gehört. @@ -151,14 +175,20 @@ export default function WebsitesPage() {

Langlebigkeit

- Modernste Web-Technologien für maximale Performance und Wartbarkeit. + + Modernste Web-Technologien für maximale Performance und + Wartbarkeit. +

Resilienz

- Minimale Angriffsfläche durch Verzicht auf unnötige Drittanbieter-Software. + + Minimale Angriffsfläche durch Verzicht auf unnötige + Drittanbieter-Software. +
@@ -167,8 +197,8 @@ export default function WebsitesPage() { {/* Content/Tech Separation */} -
- Sie können Texte und Bilder selbst anpassen, ohne das Design oder die Technik zu gefährden. - Ein intuitives System sorgt dafür, dass alles an seinem Platz bleibt. + Sie können Texte und Bilder selbst anpassen, ohne das Design + oder die Technik zu gefährden. Ein{" "} + intuitives System{" "} + sorgt dafür, dass alles an seinem Platz bleibt. @@ -205,8 +237,12 @@ export default function WebsitesPage() {
-
Design-Chaos
-
Technische Fehler
+
+ Design-Chaos +
+
+ Technische Fehler +
@@ -217,8 +253,8 @@ export default function WebsitesPage() {
{/* Simple Changes */} -
} @@ -233,23 +269,37 @@ export default function WebsitesPage() { Ihr Business entwickelt sich weiter, Ihre Website auch.
- Keine komplizierten Prozesse, sondern direkte Umsetzung Ihrer Ideen. + Keine komplizierten Prozesse, sondern{" "} + direkte Umsetzung Ihrer + Ideen.
- +

Direkter Draht

- Sie sprechen direkt mit dem Entwickler. Keine Stille Post. + + Sie sprechen direkt mit dem Entwickler. Keine Stille Post. +
- +

Agile Anpassung

- Schnelle Iterationen statt langer Wartezeiten. + + Schnelle Iterationen statt langer Wartezeiten. +
@@ -258,8 +308,8 @@ export default function WebsitesPage() {
{/* Result */} -
{[ - { title: 'Kein Overhead', desc: 'Fokus auf das, was Ihre Kunden wirklich brauchen.' }, - { title: 'Volle Kontrolle', desc: 'Der Code gehört Ihnen, ohne Vendor Lock-in.' }, - { title: 'Echte Performance', desc: 'Messbare Geschwindigkeit für bessere Ergebnisse.' }, + { + title: "Kein Overhead", + desc: "Fokus auf das, was Ihre Kunden wirklich brauchen.", + }, + { + title: "Volle Kontrolle", + desc: "Der Code gehört Ihnen, ohne Vendor Lock-in.", + }, + { + title: "Echte Performance", + desc: "Messbare Geschwindigkeit für bessere Ergebnisse.", + }, ].map((item, i) => (

{item.title}

- {item.desc} + + {item.desc} +
))} @@ -297,9 +358,7 @@ export default function WebsitesPage() { Lassen Sie uns über Ihr nächstes Projekt sprechen.
- - Projekt anfragen - + Projekt anfragen
diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index ddd3a00..dae3127 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -4,6 +4,10 @@ import withMintelConfig from "@mintel/next-config"; const nextConfig = { reactStrictMode: true, output: 'standalone', + images: { + loader: 'custom', + loaderFile: './src/utils/imgproxy-loader.ts', + }, async rewrites() { const umamiUrl = process.env.UMAMI_API_ENDPOINT || diff --git a/apps/web/src/components/IconList.tsx b/apps/web/src/components/IconList.tsx new file mode 100644 index 0000000..dc7fcbb --- /dev/null +++ b/apps/web/src/components/IconList.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { Check } from "lucide-react"; + +interface IconListProps { + children: React.ReactNode; + className?: string; +} + +interface IconListItemProps { + children: React.ReactNode; + icon?: React.ReactNode; + bullet?: boolean; + check?: boolean; + className?: string; + iconClassName?: string; + iconContainerClassName?: string; +} + +export const IconList: React.FC = ({ + children, + className = "", +}) => ; + +export const IconListItem: React.FC = ({ + children, + icon, + bullet, + check, + className = "", + iconClassName = "", + iconContainerClassName = "", +}) => { + let renderIcon = icon; + + if (bullet) { + renderIcon =
; + } else if (check) { + renderIcon = ( +
+ +
+ ); + } + + return ( +
  • + {renderIcon && ( +
    + {renderIcon} +
    + )} +
    {children}
    +
  • + ); +}; diff --git a/apps/web/src/utils/imgproxy-loader.ts b/apps/web/src/utils/imgproxy-loader.ts new file mode 100644 index 0000000..643871a --- /dev/null +++ b/apps/web/src/utils/imgproxy-loader.ts @@ -0,0 +1,26 @@ +import { getImgproxyUrl } from "./imgproxy"; + +/** + * Next.js Image Loader for imgproxy + * + * @param {Object} props - properties from Next.js Image component + * @param {string} props.src - The source image URL + * @param {number} props.width - The desired image width + * @param {number} props.quality - The desired image quality (ignored for now as imgproxy handles it) + */ +export default function imgproxyLoader({ + src, + width, + quality, +}: { + src: string; + width: number; + quality?: number; +}) { + // We use the width provided by Next.js for responsive images + // Height is set to 0 to maintain aspect ratio + return getImgproxyUrl(src, { + width, + resizing_type: "fit", + }); +} diff --git a/apps/web/src/utils/imgproxy.ts b/apps/web/src/utils/imgproxy.ts new file mode 100644 index 0000000..04d07ed --- /dev/null +++ b/apps/web/src/utils/imgproxy.ts @@ -0,0 +1,79 @@ +/** + * Generates an imgproxy URL for a given source image and options. + * + * Documentation: https://docs.imgproxy.net/usage/processing + */ + +interface ImgproxyOptions { + width?: number; + height?: number; + resizing_type?: "fit" | "fill" | "fill-down" | "force" | "auto"; + gravity?: string; + enlarge?: boolean; + extension?: string; +} + +/** + * Encodes a string to Base64 (URL-safe) + */ +function encodeBase64(str: string): string { + if (typeof Buffer !== "undefined") { + return Buffer.from(str) + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); + } else { + // Fallback for browser environment if Buffer is not available + return window + .btoa(str) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); + } +} + +export function getImgproxyUrl( + src: string, + options: ImgproxyOptions = {}, +): string { + const baseUrl = + process.env.NEXT_PUBLIC_IMGPROXY_URL || "https://img.infra.mintel.me"; + + // If no imgproxy URL is configured, return the source as is + if (!baseUrl) return src; + + // Handle local paths or relative URLs + let absoluteSrc = src; + if (src.startsWith("/")) { + const baseUrl = + process.env.NEXT_PUBLIC_BASE_URL || + (typeof window !== "undefined" ? window.location.origin : ""); + if (baseUrl) { + absoluteSrc = `${baseUrl}${src}`; + } + } + + const { + width = 0, + height = 0, + resizing_type = "fit", + gravity = "sm", + enlarge = false, + extension = "", + } = options; + + // Processing options + // Format: /rs::::/g: + const processingOptions = [ + `rs:${resizing_type}:${width}:${height}:${enlarge ? 1 : 0}`, + `g:${gravity}`, + ].join("/"); + + // Using /unsafe/ for now as we don't handle signatures yet + // Format: /unsafe// + const suffix = extension ? `@${extension}` : ""; + const encodedSrc = encodeBase64(absoluteSrc + suffix); + + return `${baseUrl}/unsafe/${processingOptions}/${encodedSrc}`; +} diff --git a/apps/web/src/utils/imgproxy.verify.ts b/apps/web/src/utils/imgproxy.verify.ts new file mode 100644 index 0000000..302b87e --- /dev/null +++ b/apps/web/src/utils/imgproxy.verify.ts @@ -0,0 +1,62 @@ +import { getImgproxyUrl } from "./imgproxy"; + +/** + * Verification script for imgproxy URL generation + */ + +function testImgproxyUrl() { + const testCases = [ + { + src: "https://picsum.photos/800/800", + options: { width: 400, height: 300, resizing_type: "fill" as const }, + description: "Remote URL with fill resizing", + }, + { + src: "/images/avatar.jpg", + options: { width: 100, extension: "webp" }, + description: "Local path with extension conversion", + }, + { + src: "https://example.com/image.png", + options: { gravity: "no" }, + description: "Remote URL with custom gravity", + }, + ]; + + console.log("🧪 Testing imgproxy URL generation...\n"); + + testCases.forEach((tc, i) => { + const url = getImgproxyUrl(tc.src, tc.options); + console.log(`Test Case ${i + 1}: ${tc.description}`); + console.log(`Source: ${tc.src}`); + console.log(`Result: ${url}`); + + // Basic validation + if (url.startsWith("https://img.infra.mintel.me/unsafe/")) { + console.log("✅ Base URL and unsafe path correct"); + } else { + console.log("❌ Base URL or unsafe path mismatch"); + } + + if ( + tc.options.width && + url.includes( + `rs:${tc.options.resizing_type || "fit"}:${tc.options.width}`, + ) + ) { + console.log("✅ Resizing options present"); + } + + console.log("-------------------\n"); + }); +} + +// Mock environment for testing if not set +if (!process.env.NEXT_PUBLIC_IMGPROXY_URL) { + process.env.NEXT_PUBLIC_IMGPROXY_URL = "https://img.infra.mintel.me"; +} +if (!process.env.NEXT_PUBLIC_BASE_URL) { + process.env.NEXT_PUBLIC_BASE_URL = "http://mintel.localhost"; +} + +testImgproxyUrl(); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index ae37b91..b7fa5e1 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -21,7 +21,6 @@ services: gatekeeper: profiles: ["gatekeeper"] image: registry.infra.mintel.me/mintel/gatekeeper:v1.7.12 - container_name: ${PROJECT_NAME:-mintel-me}-gatekeeper restart: always networks: infra: @@ -80,6 +79,28 @@ services: volumes: - directus-db-data:/var/lib/postgresql/data + imgproxy: + image: darthsim/imgproxy:latest + restart: always + networks: + - default + - infra + extra_hosts: + - "mintel.localhost:host-gateway" + - "cms.mintel.localhost:host-gateway" + - "host.docker.internal:host-gateway" + environment: + IMGPROXY_USE_ETAG: "true" + IMGPROXY_MAX_SRC_RESOLUTION: 20 + IMGPROXY_ALLOWED_NETWORKS: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" + IMGPROXY_URL_MAPPING: "http://mintel.localhost/:http://app:3000/,http://cms.mintel.localhost/:http://directus:8055/" + labels: + - "traefik.enable=true" + - 'traefik.http.routers.${PROJECT_NAME:-mintel-me}-imgproxy.rule=Host("${IMGPROXY_HOST:-img.mintel.localhost}")' + - "traefik.http.routers.${PROJECT_NAME:-mintel-me}-imgproxy.entrypoints=web" + - "traefik.http.services.${PROJECT_NAME:-mintel-me}-imgproxy.loadbalancer.server.port=8080" + - "traefik.docker.network=infra" + networks: default: name: ${PROJECT_NAME:-mintel-me}-internal diff --git a/docker-compose.yml b/docker-compose.yml index bde140e..4e31011 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -145,6 +145,7 @@ services: networks: default: name: ${PROJECT_NAME:-mintel-me}-internal + external: true infra: external: true diff --git a/package.json b/package.json index 84b3e31..833eefa 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "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://mintel.localhost\\n🗄️ CMS: http://cms.mintel.localhost/admin\\n🚦 Traefik: http://localhost:8080\\n\\n' && docker-compose -f docker-compose.dev.yml down --remove-orphans && (docker-compose -f docker-compose.dev.yml up app directus directus-db gatekeeper & pnpm -r dev)", + "dev": "docker network create infra 2>/dev/null || true && echo \"\n🚀 Development Environment Starting...\n\n📱 App: http://mintel.localhost\n🗄️ CMS: http://cms.mintel.localhost/admin\n🖼️ Imgproxy: http://img.mintel.localhost\n🚦 Traefik: http://localhost:8080\n\" && lsof -ti:3000 | xargs kill -9 2>/dev/null || true && rm -f apps/web/.next/dev/lock 2>/dev/null || true && docker rm -f mintel-me-gatekeeper 2>/dev/null || true && COMPOSE_PROJECT_NAME=mintel-me docker-compose -f docker-compose.dev.yml down --remove-orphans && COMPOSE_PROJECT_NAME=mintel-me docker-compose -f docker-compose.dev.yml up -d app directus directus-db gatekeeper imgproxy && pnpm -r dev", "dev:local": "pnpm -r dev", "build": "pnpm -r build", "start": "pnpm -r start",