This commit is contained in:
5
.eslintignore
Normal file
5
.eslintignore
Normal file
@@ -0,0 +1,5 @@
|
||||
.next/
|
||||
node_modules/
|
||||
reference/
|
||||
public/
|
||||
dist/
|
||||
12
.eslintrc.json
Normal file
12
.eslintrc.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"prefer-const": "warn",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"@next/next/no-img-element": "warn"
|
||||
}
|
||||
}
|
||||
|
||||
17
README.md
17
README.md
@@ -38,7 +38,20 @@ SITE_URL=https://klz-cables.com
|
||||
RESEND_API_KEY=your_resend_key
|
||||
TURNSTILE_SITE_KEY=your_turnstile_key
|
||||
TURNSTILE_SECRET_KEY=your_turnstile_secret
|
||||
VERCEL_ANALYTICS_ID=your_analytics_id
|
||||
|
||||
# Umami
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID=your_umami_website_id
|
||||
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
|
||||
|
||||
# GlitchTip (Sentry compatible)
|
||||
SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
|
||||
NEXT_PUBLIC_SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
|
||||
|
||||
# Redis (optional cache)
|
||||
# Platform provides a shared redis container reachable as `redis`.
|
||||
# Pick a dedicated DB index per app, e.g. redis://redis:6379/2
|
||||
REDIS_URL=redis://redis:6379/2
|
||||
REDIS_KEY_PREFIX=klz:
|
||||
```
|
||||
|
||||
## 📊 Project Overview
|
||||
@@ -346,4 +359,4 @@ Proprietary - KLZ Cables
|
||||
|
||||
**Status**: ✅ **READY FOR DEPLOYMENT**
|
||||
**Version**: 1.0.0
|
||||
**Last Updated**: December 27, 2025
|
||||
**Last Updated**: December 27, 2025
|
||||
|
||||
@@ -3,6 +3,8 @@ import {getMessages, getTranslations} from 'next-intl/server';
|
||||
import '../../styles/globals.css';
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
import UmamiScript from '@/components/analytics/UmamiScript';
|
||||
import AnalyticsProvider from '@/components/analytics/AnalyticsProvider';
|
||||
import { Metadata, Viewport } from 'next';
|
||||
|
||||
export async function generateMetadata({params: {locale}}: {params: {locale: string}}): Promise<Metadata> {
|
||||
@@ -79,6 +81,12 @@ export default async function LocaleLayout({
|
||||
</main>
|
||||
<Footer />
|
||||
</NextIntlClientProvider>
|
||||
|
||||
{/* Loads Umami only when NEXT_PUBLIC_UMAMI_WEBSITE_ID is set */}
|
||||
<UmamiScript />
|
||||
|
||||
{/* Sends pageviews for client-side navigations */}
|
||||
<AnalyticsProvider />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
20
components/analytics/AnalyticsProvider.tsx
Normal file
20
components/analytics/AnalyticsProvider.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { getAppServices } from '@/lib/services/create-services';
|
||||
|
||||
// Minimal client-side hook that sends Umami pageviews on route changes.
|
||||
export default function AnalyticsProvider() {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
const services = getAppServices();
|
||||
const url = `${pathname}${searchParams?.size ? `?${searchParams.toString()}` : ''}`;
|
||||
services.analytics.trackPageview(url);
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
19
components/analytics/UmamiScript.tsx
Normal file
19
components/analytics/UmamiScript.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import Script from 'next/script';
|
||||
|
||||
export default function UmamiScript() {
|
||||
const websiteId = process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID;
|
||||
if (!websiteId) return null;
|
||||
|
||||
const src =
|
||||
process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL ??
|
||||
'https://analytics.infra.mintel.me/script.js';
|
||||
|
||||
return (
|
||||
<Script
|
||||
src={src}
|
||||
data-website-id={websiteId}
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,30 +4,44 @@ services:
|
||||
restart: always
|
||||
networks:
|
||||
- infra
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "-L", "http://localhost:3000/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
# Shared platform services are reachable on the `infra` network.
|
||||
# Pick an app-specific Redis DB index.
|
||||
environment:
|
||||
# Umami
|
||||
- NEXT_PUBLIC_UMAMI_WEBSITE_ID=${NEXT_PUBLIC_UMAMI_WEBSITE_ID}
|
||||
- NEXT_PUBLIC_UMAMI_SCRIPT_URL=${NEXT_PUBLIC_UMAMI_SCRIPT_URL:-https://analytics.infra.mintel.me/script.js}
|
||||
|
||||
# GlitchTip (Sentry protocol)
|
||||
- SENTRY_DSN=${SENTRY_DSN}
|
||||
- NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN:-${SENTRY_DSN}}
|
||||
|
||||
- REDIS_URL=${REDIS_URL:-redis://redis:6379/2}
|
||||
- REDIS_KEY_PREFIX=${REDIS_KEY_PREFIX:-klz:}
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
|
||||
# HTTP → HTTPS redirect
|
||||
- "traefik.http.routers.klz-cables-web.rule=Host(`klz-cables.com`) || Host(`www.klz-cables.com`)"
|
||||
# HTTP → HTTPS redirect (ohne ACME-Challenge)
|
||||
- "traefik.http.routers.klz-cables-web.rule=(Host(`klz-cables.com`) || Host(`www.klz-cables.com`)) && !PathPrefix(`/.well-known/acme-challenge/`)"
|
||||
- "traefik.http.routers.klz-cables-web.entrypoints=web"
|
||||
- "traefik.http.routers.klz-cables-web.middlewares=redirect-https"
|
||||
|
||||
# HTTPS router
|
||||
- "traefik.http.routers.klz-cables.rule=Host(`klz-cables.com`) || Host(`www.klz-cables.com`)"
|
||||
- "traefik.http.routers.klz-cables.entrypoints=websecure"
|
||||
- "traefik.http.routers.klz-cables.tls.certresolver=le"
|
||||
- "traefik.http.routers.klz-cables.tls=true"
|
||||
- "traefik.http.services.klz-cables.loadbalancer.server.port=3000"
|
||||
|
||||
# compression
|
||||
- "traefik.http.routers.klz-cables.middlewares=compress"
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
- "traefik.http.services.klz-cables.loadbalancer.server.scheme=http"
|
||||
# Forwarded Headers (für Apps, die HTTPS erwarten)
|
||||
- "traefik.http.middlewares.klz-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
|
||||
- "traefik.http.middlewares.klz-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
|
||||
# Middlewares anhängen
|
||||
- "traefik.http.routers.klz-cables.middlewares=klz-forward,compress"
|
||||
networks:
|
||||
infra:
|
||||
external: true
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
{
|
||||
ignores: [
|
||||
'.next/**',
|
||||
'node_modules/**',
|
||||
'reference/**',
|
||||
'public/**',
|
||||
'dist/**',
|
||||
],
|
||||
},
|
||||
...compat.extends('next/core-web-vitals'),
|
||||
...compat.extends('next/typescript'),
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
'prefer-const': 'warn',
|
||||
'react/no-unescaped-entities': 'off',
|
||||
'@next/next/no-img-element': 'warn',
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
17
instrumentation.ts
Normal file
17
instrumentation.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
// Next.js will call this on boot for the active runtime.
|
||||
// We dynamically import the correct Sentry config file.
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
await import('./sentry.server.config');
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||
await import('./sentry.edge.config');
|
||||
}
|
||||
}
|
||||
|
||||
// Capture errors from Server Components, middleware and route handlers.
|
||||
export const onRequestError = Sentry.captureRequestError;
|
||||
|
||||
10
lib/services/analytics/analytics-service.ts
Normal file
10
lib/services/analytics/analytics-service.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export type AnalyticsEventProperties = Record<
|
||||
string,
|
||||
string | number | boolean | null | undefined
|
||||
>;
|
||||
|
||||
export interface AnalyticsService {
|
||||
track(eventName: string, props?: AnalyticsEventProperties): void;
|
||||
trackPageview(url?: string): void;
|
||||
}
|
||||
|
||||
11
lib/services/analytics/noop-analytics-service.ts
Normal file
11
lib/services/analytics/noop-analytics-service.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { AnalyticsEventProperties, AnalyticsService } from './analytics-service';
|
||||
|
||||
export class NoopAnalyticsService implements AnalyticsService {
|
||||
track(_eventName: string, _props?: AnalyticsEventProperties) {
|
||||
// intentionally noop
|
||||
}
|
||||
|
||||
trackPageview(_url?: string) {
|
||||
// intentionally noop
|
||||
}
|
||||
}
|
||||
32
lib/services/analytics/umami-analytics-service.ts
Normal file
32
lib/services/analytics/umami-analytics-service.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { AnalyticsEventProperties, AnalyticsService } from './analytics-service';
|
||||
|
||||
type UmamiGlobal = {
|
||||
track?: (eventOrUrl: string, props?: AnalyticsEventProperties) => void;
|
||||
};
|
||||
|
||||
export type UmamiAnalyticsServiceOptions = {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export class UmamiAnalyticsService implements AnalyticsService {
|
||||
constructor(private readonly options: UmamiAnalyticsServiceOptions) {}
|
||||
|
||||
track(eventName: string, props?: AnalyticsEventProperties) {
|
||||
if (!this.options.enabled) return;
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const umami = (window as unknown as { umami?: UmamiGlobal }).umami;
|
||||
umami?.track?.(eventName, props);
|
||||
}
|
||||
|
||||
trackPageview(url?: string) {
|
||||
if (!this.options.enabled) return;
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const umami = (window as unknown as { umami?: UmamiGlobal }).umami;
|
||||
|
||||
// Umami treats `track(url)` as a pageview override.
|
||||
if (url) umami?.track?.(url);
|
||||
else umami?.track?.(window.location.pathname + window.location.search);
|
||||
}
|
||||
}
|
||||
12
lib/services/app-services.ts
Normal file
12
lib/services/app-services.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { AnalyticsService } from './analytics/analytics-service';
|
||||
import type { CacheService } from './cache/cache-service';
|
||||
import type { ErrorReportingService } from './errors/error-reporting-service';
|
||||
|
||||
// Simple constructor-based DI container.
|
||||
export class AppServices {
|
||||
constructor(
|
||||
public readonly analytics: AnalyticsService,
|
||||
public readonly errors: ErrorReportingService,
|
||||
public readonly cache: CacheService
|
||||
) {}
|
||||
}
|
||||
10
lib/services/cache/cache-service.ts
vendored
Normal file
10
lib/services/cache/cache-service.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
export type CacheSetOptions = {
|
||||
ttlSeconds?: number;
|
||||
};
|
||||
|
||||
export interface CacheService {
|
||||
get<T>(key: string): Promise<T | undefined>;
|
||||
set<T>(key: string, value: T, options?: CacheSetOptions): Promise<void>;
|
||||
del(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
30
lib/services/cache/memory-cache-service.ts
vendored
Normal file
30
lib/services/cache/memory-cache-service.ts
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { CacheService, CacheSetOptions } from './cache-service';
|
||||
|
||||
type Entry = {
|
||||
value: unknown;
|
||||
expiresAt?: number;
|
||||
};
|
||||
|
||||
export class MemoryCacheService implements CacheService {
|
||||
private readonly store = new Map<string, Entry>();
|
||||
|
||||
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 entry.value as T;
|
||||
}
|
||||
|
||||
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 del(key: string) {
|
||||
this.store.delete(key);
|
||||
}
|
||||
}
|
||||
49
lib/services/cache/redis-cache-service.ts
vendored
Normal file
49
lib/services/cache/redis-cache-service.ts
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
import { createClient, type RedisClientType } from 'redis';
|
||||
import type { CacheService, CacheSetOptions } from './cache-service';
|
||||
|
||||
export type RedisCacheServiceOptions = {
|
||||
url: string;
|
||||
keyPrefix?: string;
|
||||
};
|
||||
|
||||
// Thin wrapper around shared Redis (platform provides host `redis`).
|
||||
// Values are JSON-serialized.
|
||||
export class RedisCacheService implements CacheService {
|
||||
private readonly client: RedisClientType;
|
||||
private readonly keyPrefix: string;
|
||||
|
||||
constructor(options: RedisCacheServiceOptions) {
|
||||
this.client = createClient({ url: options.url });
|
||||
this.keyPrefix = options.keyPrefix ?? '';
|
||||
|
||||
// Fire-and-forget connect.
|
||||
this.client.connect().catch(() => undefined);
|
||||
}
|
||||
|
||||
private k(key: string) {
|
||||
return `${this.keyPrefix}${key}`;
|
||||
}
|
||||
|
||||
async get<T>(key: string): Promise<T | undefined> {
|
||||
const raw = await this.client.get(this.k(key));
|
||||
if (raw == null) return undefined;
|
||||
return JSON.parse(raw) as T;
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T, options?: CacheSetOptions): Promise<void> {
|
||||
const ttl = options?.ttlSeconds;
|
||||
const raw = JSON.stringify(value);
|
||||
|
||||
if (ttl && ttl > 0) {
|
||||
await this.client.set(this.k(key), raw, { EX: ttl });
|
||||
return;
|
||||
}
|
||||
|
||||
await this.client.set(this.k(key), raw);
|
||||
}
|
||||
|
||||
async del(key: string): Promise<void> {
|
||||
await this.client.del(this.k(key));
|
||||
}
|
||||
}
|
||||
|
||||
44
lib/services/create-services.ts
Normal file
44
lib/services/create-services.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
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 { RedisCacheService } from './cache/redis-cache-service';
|
||||
import { GlitchtipErrorReportingService } from './errors/glitchtip-error-reporting-service';
|
||||
import { NoopErrorReportingService } from './errors/noop-error-reporting-service';
|
||||
|
||||
let singleton: AppServices | undefined;
|
||||
|
||||
export function getAppServices(): AppServices {
|
||||
// In Next.js, module singletons are per-process (server) and per-tab (client).
|
||||
// This is good enough for a small service layer.
|
||||
if (singleton) return singleton;
|
||||
|
||||
const umamiEnabled = Boolean(process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID);
|
||||
const sentryClientEnabled = Boolean(process.env.NEXT_PUBLIC_SENTRY_DSN);
|
||||
const sentryServerEnabled = Boolean(process.env.SENTRY_DSN);
|
||||
|
||||
const analytics = umamiEnabled
|
||||
? new UmamiAnalyticsService({ enabled: true })
|
||||
: new NoopAnalyticsService();
|
||||
|
||||
// Enable GlitchTip/Sentry only when a DSN is present for the active runtime.
|
||||
const errors =
|
||||
typeof window === 'undefined'
|
||||
? sentryServerEnabled
|
||||
? new GlitchtipErrorReportingService({ enabled: true })
|
||||
: new NoopErrorReportingService()
|
||||
: sentryClientEnabled
|
||||
? new GlitchtipErrorReportingService({ enabled: true })
|
||||
: new NoopErrorReportingService();
|
||||
|
||||
const redisUrl = process.env.REDIS_URL;
|
||||
const cache = redisUrl
|
||||
? new RedisCacheService({
|
||||
url: redisUrl,
|
||||
keyPrefix: process.env.REDIS_KEY_PREFIX ?? 'klz:',
|
||||
})
|
||||
: new MemoryCacheService();
|
||||
|
||||
singleton = new AppServices(analytics, errors, cache);
|
||||
return singleton;
|
||||
}
|
||||
16
lib/services/errors/error-reporting-service.ts
Normal file
16
lib/services/errors/error-reporting-service.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
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>): string | undefined;
|
||||
captureMessage(message: string, level?: ErrorReportingLevel): string | undefined;
|
||||
setUser(user: ErrorReportingUser | null): void;
|
||||
setTag(key: string, value: string): void;
|
||||
withScope<T>(fn: () => T, context?: Record<string, unknown>): T;
|
||||
}
|
||||
|
||||
53
lib/services/errors/glitchtip-error-reporting-service.ts
Normal file
53
lib/services/errors/glitchtip-error-reporting-service.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import type {
|
||||
ErrorReportingLevel,
|
||||
ErrorReportingService,
|
||||
ErrorReportingUser,
|
||||
} from './error-reporting-service';
|
||||
|
||||
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 options: GlitchtipErrorReportingServiceOptions,
|
||||
private readonly sentry: SentryLike = Sentry
|
||||
) {}
|
||||
|
||||
captureException(error: unknown, context?: Record<string, unknown>) {
|
||||
if (!this.options.enabled) return undefined;
|
||||
return this.sentry.captureException(error, context as any) as any;
|
||||
}
|
||||
|
||||
captureMessage(message: string, level: ErrorReportingLevel = 'error') {
|
||||
if (!this.options.enabled) return undefined;
|
||||
return this.sentry.captureMessage(message, level as any) as any;
|
||||
}
|
||||
|
||||
setUser(user: ErrorReportingUser | null) {
|
||||
if (!this.options.enabled) return;
|
||||
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) {
|
||||
for (const [key, value] of Object.entries(context)) {
|
||||
scope.setExtra(key, value);
|
||||
}
|
||||
}
|
||||
return fn();
|
||||
});
|
||||
}
|
||||
}
|
||||
18
lib/services/errors/noop-error-reporting-service.ts
Normal file
18
lib/services/errors/noop-error-reporting-service.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { ErrorReportingLevel, ErrorReportingService, ErrorReportingUser } from './error-reporting-service';
|
||||
|
||||
export class NoopErrorReportingService implements ErrorReportingService {
|
||||
captureException(_error: unknown, _context?: Record<string, unknown>) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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,8 @@
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
|
||||
import { withSentryConfig } from '@sentry/nextjs';
|
||||
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
@@ -19,5 +20,21 @@ const nextConfig = {
|
||||
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
|
||||
},
|
||||
};
|
||||
|
||||
export default withNextIntl(nextConfig);
|
||||
|
||||
const nextIntlConfig = withNextIntl(nextConfig);
|
||||
|
||||
// GlitchTip is Sentry-compatible; we use the Sentry Next.js SDK.
|
||||
// Source map upload is optional; we keep this config minimal.
|
||||
export default withSentryConfig(
|
||||
nextIntlConfig,
|
||||
{
|
||||
silent: !process.env.CI,
|
||||
// Keep bundle size down; remove SDK debug logging.
|
||||
treeshake: { removeDebugLogging: true },
|
||||
},
|
||||
// Sentry Webpack plugin options (not needed unless you upload sourcemaps)
|
||||
{
|
||||
// no sourcemap upload by default
|
||||
authToken: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
3291
package-lock.json
generated
3291
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@react-pdf/renderer": "^4.3.2",
|
||||
"@sentry/nextjs": "^8.55.0",
|
||||
"@swc/helpers": "^0.5.18",
|
||||
"@types/cheerio": "^0.22.35",
|
||||
"axios": "^1.13.2",
|
||||
@@ -18,6 +19,7 @@
|
||||
"pdf-lib": "^1.17.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"redis": "^4.7.1",
|
||||
"resend": "^3.5.0",
|
||||
"sharp": "^0.34.5",
|
||||
"svg-to-pdfkit": "^0.1.8",
|
||||
@@ -33,8 +35,8 @@
|
||||
"@types/sharp": "^0.31.1",
|
||||
"@vitest/ui": "^4.0.16",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-next": "15.1.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "14.2.35",
|
||||
"postcss": "^8.5.6",
|
||||
"sass": "^1.97.1",
|
||||
"tailwindcss": "^4.1.18",
|
||||
@@ -50,7 +52,7 @@
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"pdf:datasheets": "tsx ./scripts/generate-pdf-datasheets.ts",
|
||||
"pdf:datasheets:legacy": "tsx ./scripts/generate-pdf-datasheets-pdf-lib.ts"
|
||||
},
|
||||
|
||||
10
sentry.client.config.ts
Normal file
10
sentry.client.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn,
|
||||
enabled: Boolean(dsn),
|
||||
tracesSampleRate: 0,
|
||||
});
|
||||
|
||||
10
sentry.edge.config.ts
Normal file
10
sentry.edge.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
const dsn = process.env.SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn,
|
||||
enabled: Boolean(dsn),
|
||||
tracesSampleRate: 0,
|
||||
});
|
||||
|
||||
10
sentry.server.config.ts
Normal file
10
sentry.server.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
const dsn = process.env.SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn,
|
||||
enabled: Boolean(dsn),
|
||||
tracesSampleRate: 0,
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
22
types/redis.d.ts
vendored
Normal file
22
types/redis.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
// Fallback ambient types for `redis`.
|
||||
//
|
||||
// The official `redis` package ships its own types. In some editor setups
|
||||
// (especially with newer TS + `moduleResolution: bundler`) the TS server may
|
||||
// temporarily fail to resolve them. This keeps the project compiling.
|
||||
declare module 'redis' {
|
||||
export type RedisClientType = {
|
||||
connect(): Promise<void>;
|
||||
get(key: string): Promise<string | null>;
|
||||
set(
|
||||
key: string,
|
||||
value: string,
|
||||
options?: {
|
||||
EX?: number;
|
||||
}
|
||||
): Promise<unknown>;
|
||||
del(key: string): Promise<number>;
|
||||
};
|
||||
|
||||
export function createClient(options: { url: string }): RedisClientType;
|
||||
}
|
||||
|
||||
43
types/sentry-nextjs.d.ts
vendored
Normal file
43
types/sentry-nextjs.d.ts
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
// Fallback ambient types for `@sentry/nextjs`.
|
||||
//
|
||||
// In case the editor/TS server fails to pick up the package's bundled types
|
||||
// (common in some TS + moduleResolution combinations), this keeps the repo
|
||||
// type-safe enough to build.
|
||||
//
|
||||
// If your tooling resolves the official types correctly, this file is still
|
||||
// compatible (it just provides broad typings).
|
||||
declare module '@sentry/nextjs' {
|
||||
export type SeverityLevel =
|
||||
| 'fatal'
|
||||
| 'error'
|
||||
| 'warning'
|
||||
| 'info'
|
||||
| 'debug'
|
||||
| 'log';
|
||||
|
||||
export type User = {
|
||||
id?: string;
|
||||
email?: string;
|
||||
username?: string;
|
||||
};
|
||||
|
||||
export function init(options: any): void;
|
||||
export function captureException(exception: any, captureContext?: any): any;
|
||||
export function captureMessage(message: string, level?: SeverityLevel): any;
|
||||
export function setUser(user: User | null): void;
|
||||
export function setTag(key: string, value: string): void;
|
||||
export function withScope<T>(callback: (scope: any) => T): T;
|
||||
|
||||
export const captureRequestError: (...args: any[]) => any;
|
||||
export const captureRouterTransitionStart: (...args: any[]) => any;
|
||||
|
||||
export function replayIntegration(...args: any[]): any;
|
||||
export function feedbackIntegration(...args: any[]): any;
|
||||
|
||||
export function withSentryConfig(
|
||||
nextConfig: any,
|
||||
sentryWebpackPluginOptions?: any,
|
||||
sentryBuildOptions?: any
|
||||
): any;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user