full upgrade to dev
This commit is contained in:
@@ -4,30 +4,30 @@
|
||||
export class PrismaClient {
|
||||
project = {
|
||||
findMany: jest.fn(async () => []),
|
||||
findUnique: jest.fn(async (args: any) => null),
|
||||
findUnique: jest.fn(async (_args: unknown) => null),
|
||||
count: jest.fn(async () => 0),
|
||||
create: jest.fn(async (data: any) => data),
|
||||
update: jest.fn(async (data: any) => data),
|
||||
delete: jest.fn(async (data: any) => data),
|
||||
updateMany: jest.fn(async (data: any) => ({})),
|
||||
create: jest.fn(async (data: unknown) => data),
|
||||
update: jest.fn(async (data: unknown) => data),
|
||||
delete: jest.fn(async (data: unknown) => data),
|
||||
updateMany: jest.fn(async (_data: unknown) => ({})),
|
||||
};
|
||||
|
||||
contact = {
|
||||
create: jest.fn(async (data: any) => data),
|
||||
create: jest.fn(async (data: unknown) => data),
|
||||
findMany: jest.fn(async () => []),
|
||||
count: jest.fn(async () => 0),
|
||||
update: jest.fn(async (data: any) => data),
|
||||
delete: jest.fn(async (data: any) => data),
|
||||
update: jest.fn(async (data: unknown) => data),
|
||||
delete: jest.fn(async (data: unknown) => data),
|
||||
};
|
||||
|
||||
pageView = {
|
||||
create: jest.fn(async (data: any) => data),
|
||||
create: jest.fn(async (data: unknown) => data),
|
||||
count: jest.fn(async () => 0),
|
||||
deleteMany: jest.fn(async () => ({})),
|
||||
};
|
||||
|
||||
userInteraction = {
|
||||
create: jest.fn(async (data: any) => data),
|
||||
create: jest.fn(async (data: unknown) => data),
|
||||
groupBy: jest.fn(async () => []),
|
||||
deleteMany: jest.fn(async () => ({})),
|
||||
};
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
jest.mock('next/server', () => ({
|
||||
jest.mock("next/server", () => ({
|
||||
NextResponse: jest.fn().mockImplementation(function (body, init) {
|
||||
// Use function and assign to `this` so `new NextResponse(...)` returns an instance with properties
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
|
||||
this.body = body;
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
|
||||
this.init = init;
|
||||
}),
|
||||
}));
|
||||
|
||||
import { GET } from '@/app/api/sitemap/route';
|
||||
import { mockFetch } from '@/app/__tests__/__mocks__/mock-fetch';
|
||||
import { GET } from "@/app/api/sitemap/route";
|
||||
|
||||
// Mock node-fetch so we don't perform real network requests in tests
|
||||
jest.mock('node-fetch', () => ({
|
||||
jest.mock("node-fetch", () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
@@ -21,60 +20,81 @@ jest.mock('node-fetch', () => ({
|
||||
Promise.resolve({
|
||||
posts: [
|
||||
{
|
||||
id: '67ac8dfa709c60000117d312',
|
||||
title: 'Just Doing Some Testing',
|
||||
meta_description: 'Hello bla bla bla bla',
|
||||
slug: 'just-doing-some-testing',
|
||||
updated_at: '2025-02-13T14:25:38.000+00:00',
|
||||
id: "67ac8dfa709c60000117d312",
|
||||
title: "Just Doing Some Testing",
|
||||
meta_description: "Hello bla bla bla bla",
|
||||
slug: "just-doing-some-testing",
|
||||
updated_at: "2025-02-13T14:25:38.000+00:00",
|
||||
},
|
||||
{
|
||||
id: '67aaffc3709c60000117d2d9',
|
||||
title: 'Blockchain Based Voting System',
|
||||
meta_description: 'This project aims to revolutionize voting systems by leveraging blockchain to ensure security, transparency, and immutability.',
|
||||
slug: 'blockchain-based-voting-system',
|
||||
updated_at: '2025-02-13T16:54:42.000+00:00',
|
||||
id: "67aaffc3709c60000117d2d9",
|
||||
title: "Blockchain Based Voting System",
|
||||
meta_description:
|
||||
"This project aims to revolutionize voting systems by leveraging blockchain to ensure security, transparency, and immutability.",
|
||||
slug: "blockchain-based-voting-system",
|
||||
updated_at: "2025-02-13T16:54:42.000+00:00",
|
||||
},
|
||||
],
|
||||
meta: { pagination: { limit: 'all', next: null, page: 1, pages: 1, prev: null, total: 2 } },
|
||||
meta: {
|
||||
pagination: {
|
||||
limit: "all",
|
||||
next: null,
|
||||
page: 1,
|
||||
pages: 1,
|
||||
prev: null,
|
||||
total: 2,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
})
|
||||
),
|
||||
}));
|
||||
|
||||
describe('GET /api/sitemap', () => {
|
||||
describe("GET /api/sitemap", () => {
|
||||
beforeAll(() => {
|
||||
process.env.GHOST_API_URL = 'http://localhost:2368';
|
||||
process.env.GHOST_API_KEY = 'test-api-key';
|
||||
process.env.NEXT_PUBLIC_BASE_URL = 'https://dki.one';
|
||||
process.env.GHOST_API_URL = "http://localhost:2368";
|
||||
process.env.GHOST_API_KEY = "test-api-key";
|
||||
process.env.NEXT_PUBLIC_BASE_URL = "https://dki.one";
|
||||
|
||||
// Provide mock posts via env so route can use them without fetching
|
||||
process.env.GHOST_MOCK_POSTS = JSON.stringify({ posts: [
|
||||
process.env.GHOST_MOCK_POSTS = JSON.stringify({
|
||||
posts: [
|
||||
{
|
||||
id: '67ac8dfa709c60000117d312',
|
||||
title: 'Just Doing Some Testing',
|
||||
meta_description: 'Hello bla bla bla bla',
|
||||
slug: 'just-doing-some-testing',
|
||||
updated_at: '2025-02-13T14:25:38.000+00:00',
|
||||
id: "67ac8dfa709c60000117d312",
|
||||
title: "Just Doing Some Testing",
|
||||
meta_description: "Hello bla bla bla bla",
|
||||
slug: "just-doing-some-testing",
|
||||
updated_at: "2025-02-13T14:25:38.000+00:00",
|
||||
},
|
||||
{
|
||||
id: '67aaffc3709c60000117d2d9',
|
||||
title: 'Blockchain Based Voting System',
|
||||
meta_description: 'This project aims to revolutionize voting systems by leveraging blockchain to ensure security, transparency, and immutability.',
|
||||
slug: 'blockchain-based-voting-system',
|
||||
updated_at: '2025-02-13T16:54:42.000+00:00',
|
||||
id: "67aaffc3709c60000117d2d9",
|
||||
title: "Blockchain Based Voting System",
|
||||
meta_description:
|
||||
"This project aims to revolutionize voting systems by leveraging blockchain to ensure security, transparency, and immutability.",
|
||||
slug: "blockchain-based-voting-system",
|
||||
updated_at: "2025-02-13T16:54:42.000+00:00",
|
||||
},
|
||||
] });
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a sitemap', async () => {
|
||||
it("should return a sitemap", async () => {
|
||||
const response = await GET();
|
||||
|
||||
expect(response.body).toContain('<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">');
|
||||
expect(response.body).toContain('<loc>https://dki.one/</loc>');
|
||||
expect(response.body).toContain('<loc>https://dki.one/legal-notice</loc>');
|
||||
expect(response.body).toContain('<loc>https://dki.one/privacy-policy</loc>');
|
||||
expect(response.body).toContain('<loc>https://dki.one/projects/just-doing-some-testing</loc>');
|
||||
expect(response.body).toContain('<loc>https://dki.one/projects/blockchain-based-voting-system</loc>');
|
||||
expect(response.body).toContain(
|
||||
'<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">',
|
||||
);
|
||||
expect(response.body).toContain("<loc>https://dki.one/</loc>");
|
||||
expect(response.body).toContain("<loc>https://dki.one/legal-notice</loc>");
|
||||
expect(response.body).toContain(
|
||||
"<loc>https://dki.one/privacy-policy</loc>",
|
||||
);
|
||||
expect(response.body).toContain(
|
||||
"<loc>https://dki.one/projects/just-doing-some-testing</loc>",
|
||||
);
|
||||
expect(response.body).toContain(
|
||||
"<loc>https://dki.one/projects/blockchain-based-voting-system</loc>",
|
||||
);
|
||||
// Note: Headers are not available in test environment
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,10 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { GET } from '@/app/sitemap.xml/route';
|
||||
import { mockFetch } from '@/app/__tests__/__mocks__/mock-fetch-sitemap';
|
||||
import "@testing-library/jest-dom";
|
||||
import { GET } from "@/app/sitemap.xml/route";
|
||||
|
||||
jest.mock('next/server', () => ({
|
||||
jest.mock("next/server", () => ({
|
||||
NextResponse: jest.fn().mockImplementation(function (body, init) {
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
this.body = body;
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
|
||||
this.init = init;
|
||||
}),
|
||||
}));
|
||||
@@ -33,36 +31,49 @@ const sitemapXml = `
|
||||
`;
|
||||
|
||||
// Mock node-fetch for sitemap endpoint (hoisted by Jest)
|
||||
jest.mock('node-fetch', () => ({
|
||||
jest.mock("node-fetch", () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn((url: string) => Promise.resolve({ ok: true, text: () => Promise.resolve(sitemapXml) })),
|
||||
default: jest.fn((_url: string) =>
|
||||
Promise.resolve({ ok: true, text: () => Promise.resolve(sitemapXml) }),
|
||||
),
|
||||
}));
|
||||
|
||||
describe('Sitemap Component', () => {
|
||||
describe("Sitemap Component", () => {
|
||||
beforeAll(() => {
|
||||
process.env.NEXT_PUBLIC_BASE_URL = 'https://dki.one';
|
||||
process.env.NEXT_PUBLIC_BASE_URL = "https://dki.one";
|
||||
|
||||
// Provide sitemap XML directly so route uses it without fetching
|
||||
process.env.GHOST_MOCK_SITEMAP = sitemapXml;
|
||||
|
||||
// Mock global.fetch too, to avoid any network calls
|
||||
global.fetch = jest.fn().mockImplementation((url: string) => {
|
||||
if (url.includes('/api/sitemap')) {
|
||||
return Promise.resolve({ ok: true, text: () => Promise.resolve(sitemapXml) });
|
||||
if (url.includes("/api/sitemap")) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(sitemapXml),
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error(`Unknown URL: ${url}`));
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the sitemap XML', async () => {
|
||||
it("should render the sitemap XML", async () => {
|
||||
const response = await GET();
|
||||
|
||||
expect(response.body).toContain('<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">');
|
||||
expect(response.body).toContain('<loc>https://dki.one/</loc>');
|
||||
expect(response.body).toContain('<loc>https://dki.one/legal-notice</loc>');
|
||||
expect(response.body).toContain('<loc>https://dki.one/privacy-policy</loc>');
|
||||
expect(response.body).toContain('<loc>https://dki.one/projects/just-doing-some-testing</loc>');
|
||||
expect(response.body).toContain('<loc>https://dki.one/projects/blockchain-based-voting-system</loc>');
|
||||
expect(response.body).toContain(
|
||||
'<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">',
|
||||
);
|
||||
expect(response.body).toContain("<loc>https://dki.one/</loc>");
|
||||
expect(response.body).toContain("<loc>https://dki.one/legal-notice</loc>");
|
||||
expect(response.body).toContain(
|
||||
"<loc>https://dki.one/privacy-policy</loc>",
|
||||
);
|
||||
expect(response.body).toContain(
|
||||
"<loc>https://dki.one/projects/just-doing-some-testing</loc>",
|
||||
);
|
||||
expect(response.body).toContain(
|
||||
"<loc>https://dki.one/projects/blockchain-based-voting-system</loc>",
|
||||
);
|
||||
// Note: Headers are not available in test environment
|
||||
});
|
||||
});
|
||||
@@ -7,9 +7,9 @@ async function getFetch() {
|
||||
try {
|
||||
const mod = await import("node-fetch");
|
||||
// support both CJS and ESM interop
|
||||
return (mod as any).default ?? mod;
|
||||
} catch (err) {
|
||||
return (globalThis as any).fetch;
|
||||
return (mod as { default: unknown }).default ?? mod;
|
||||
} catch (_err) {
|
||||
return (globalThis as unknown as { fetch: unknown }).fetch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +49,10 @@ export async function GET() {
|
||||
const fetchFn = await getFetch();
|
||||
const response = await fetchFn(
|
||||
`${GHOST_API_URL}/ghost/api/content/posts/?key=${GHOST_API_KEY}&limit=all`,
|
||||
{ agent: agent as unknown as undefined }
|
||||
{ agent: agent as unknown as undefined },
|
||||
);
|
||||
const posts: GhostPostsResponse = await response.json() as GhostPostsResponse;
|
||||
const posts: GhostPostsResponse =
|
||||
(await response.json()) as GhostPostsResponse;
|
||||
|
||||
if (!posts || !posts.posts) {
|
||||
console.error("Invalid posts data");
|
||||
|
||||
@@ -13,22 +13,28 @@ export async function GET(req: NextRequest) {
|
||||
|
||||
try {
|
||||
// Try global fetch first, fall back to node-fetch if necessary
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let response: any;
|
||||
try {
|
||||
if (typeof (globalThis as any).fetch === 'function') {
|
||||
response = await (globalThis as any).fetch(url);
|
||||
if (
|
||||
typeof (globalThis as unknown as { fetch: unknown }).fetch ===
|
||||
"function"
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
response = await (globalThis as unknown as { fetch: any }).fetch(url);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
response = undefined;
|
||||
}
|
||||
|
||||
if (!response || typeof response.ok === 'undefined' || !response.ok) {
|
||||
if (!response || typeof response.ok === "undefined" || !response.ok) {
|
||||
try {
|
||||
const mod = await import('node-fetch');
|
||||
const nodeFetch = (mod as any).default ?? mod;
|
||||
response = await nodeFetch(url);
|
||||
const mod = await import("node-fetch");
|
||||
const nodeFetch = (mod as { default: unknown }).default ?? mod;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
response = await (nodeFetch as any)(url);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch image:', err);
|
||||
console.error("Failed to fetch image:", err);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch image" },
|
||||
{ status: 500 },
|
||||
@@ -37,7 +43,9 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
throw new Error(`Failed to fetch image: ${response?.statusText ?? 'no response'}`);
|
||||
throw new Error(
|
||||
`Failed to fetch image: ${response?.statusText ?? "no response"}`,
|
||||
);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("content-type");
|
||||
|
||||
@@ -15,40 +15,52 @@ export async function GET(request: Request) {
|
||||
|
||||
try {
|
||||
// Debug: show whether fetch is present/mocked
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('DEBUG fetch in fetchProject:', typeof (globalThis as any).fetch, 'globalIsMock:', !!(globalThis as any).fetch?._isMockFunction);
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
console.log(
|
||||
"DEBUG fetch in fetchProject:",
|
||||
typeof (globalThis as any).fetch,
|
||||
"globalIsMock:",
|
||||
!!(globalThis as any).fetch?._isMockFunction,
|
||||
);
|
||||
|
||||
// Try global fetch first (as tests often mock it). If it fails or returns undefined,
|
||||
// fall back to dynamically importing node-fetch.
|
||||
let response: any;
|
||||
|
||||
if (typeof (globalThis as any).fetch === 'function') {
|
||||
if (typeof (globalThis as any).fetch === "function") {
|
||||
try {
|
||||
response = await (globalThis as any).fetch(
|
||||
`${GHOST_API_URL}/ghost/api/content/posts/slug/${slug}/?key=${GHOST_API_KEY}`,
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
response = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (!response || typeof response.ok === 'undefined') {
|
||||
if (!response || typeof response.ok === "undefined") {
|
||||
try {
|
||||
const mod = await import('node-fetch');
|
||||
const mod = await import("node-fetch");
|
||||
const nodeFetch = (mod as any).default ?? mod;
|
||||
response = await nodeFetch(
|
||||
response = await (nodeFetch as any)(
|
||||
`${GHOST_API_URL}/ghost/api/content/posts/slug/${slug}/?key=${GHOST_API_KEY}`,
|
||||
);
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
response = undefined;
|
||||
}
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
// Debug: inspect the response returned from the fetch
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('DEBUG fetch response:', response);
|
||||
|
||||
// Debug: inspect the response returned from the fetch
|
||||
|
||||
console.log("DEBUG fetch response:", response);
|
||||
|
||||
if (!response || !response.ok) {
|
||||
throw new Error(`Failed to fetch post: ${response?.statusText ?? 'no response'}`);
|
||||
throw new Error(
|
||||
`Failed to fetch post: ${response?.statusText ?? "no response"}`,
|
||||
);
|
||||
}
|
||||
|
||||
const post = await response.json();
|
||||
|
||||
@@ -14,7 +14,6 @@ export const runtime = "nodejs"; // Force Node runtime
|
||||
|
||||
// Read Ghost API config at runtime, tests may set env vars in beforeAll
|
||||
|
||||
|
||||
// Funktion, um die XML für die Sitemap zu generieren
|
||||
function generateXml(sitemapRoutes: { url: string; lastModified: string }[]) {
|
||||
const xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>';
|
||||
@@ -63,7 +62,7 @@ export async function GET() {
|
||||
];
|
||||
|
||||
// In test environment we can short-circuit and use a mocked posts payload
|
||||
if (process.env.NODE_ENV === 'test' && process.env.GHOST_MOCK_POSTS) {
|
||||
if (process.env.NODE_ENV === "test" && process.env.GHOST_MOCK_POSTS) {
|
||||
const mockData = JSON.parse(process.env.GHOST_MOCK_POSTS);
|
||||
const projects = (mockData as ProjectsData).posts || [];
|
||||
|
||||
@@ -73,7 +72,7 @@ export async function GET() {
|
||||
url: `${baseUrl}/projects/${project.slug}`,
|
||||
lastModified,
|
||||
priority: 0.8,
|
||||
changeFreq: 'monthly',
|
||||
changeFreq: "monthly",
|
||||
};
|
||||
});
|
||||
|
||||
@@ -81,43 +80,46 @@ export async function GET() {
|
||||
const xml = generateXml(allRoutes);
|
||||
|
||||
// For tests return a plain object so tests can inspect `.body` easily
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return { body: xml, headers: { 'Content-Type': 'application/xml' } } as any;
|
||||
if (process.env.NODE_ENV === "test") {
|
||||
return {
|
||||
body: xml,
|
||||
headers: { "Content-Type": "application/xml" },
|
||||
};
|
||||
}
|
||||
|
||||
return new NextResponse(xml, {
|
||||
headers: { 'Content-Type': 'application/xml' },
|
||||
headers: { "Content-Type": "application/xml" },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Debug: show whether fetch is present/mocked
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('DEBUG fetch in sitemap API:', typeof (globalThis as any).fetch, 'globalIsMock:', !!(globalThis as any).fetch?._isMockFunction);
|
||||
|
||||
// Try global fetch first (tests may mock global.fetch)
|
||||
let response: any;
|
||||
let response: Response | undefined;
|
||||
|
||||
try {
|
||||
if (typeof (globalThis as any).fetch === 'function') {
|
||||
response = await (globalThis as any).fetch(
|
||||
if (typeof globalThis.fetch === "function") {
|
||||
response = await globalThis.fetch(
|
||||
`${process.env.GHOST_API_URL}/ghost/api/content/posts/?key=${process.env.GHOST_API_KEY}&limit=all`,
|
||||
);
|
||||
// Debug: inspect the result
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('DEBUG sitemap global fetch returned:', response);
|
||||
|
||||
console.log("DEBUG sitemap global fetch returned:", response);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
response = undefined;
|
||||
}
|
||||
|
||||
if (!response || typeof response.ok === 'undefined' || !response.ok) {
|
||||
if (!response || typeof response.ok === "undefined" || !response.ok) {
|
||||
try {
|
||||
const mod = await import('node-fetch');
|
||||
const nodeFetch = (mod as any).default ?? mod;
|
||||
const mod = await import("node-fetch");
|
||||
const nodeFetch = mod.default ?? mod;
|
||||
response = await nodeFetch(
|
||||
`${process.env.GHOST_API_URL}/ghost/api/content/posts/?key=${process.env.GHOST_API_KEY}&limit=all`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.log('Failed to fetch posts from Ghost:', err);
|
||||
console.log("Failed to fetch posts from Ghost:", err);
|
||||
return new NextResponse(generateXml(staticRoutes), {
|
||||
headers: { "Content-Type": "application/xml" },
|
||||
});
|
||||
@@ -125,13 +127,16 @@ export async function GET() {
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.error(`Failed to fetch posts: ${response?.statusText ?? 'no response'}`);
|
||||
console.error(
|
||||
`Failed to fetch posts: ${response?.statusText ?? "no response"}`,
|
||||
);
|
||||
return new NextResponse(generateXml(staticRoutes), {
|
||||
headers: { "Content-Type": "application/xml" },
|
||||
});
|
||||
}
|
||||
|
||||
const projectsData = (await response.json()) as ProjectsData;
|
||||
|
||||
const projects = projectsData.posts;
|
||||
|
||||
// Dynamische Projekt-Routen generieren
|
||||
|
||||
@@ -226,7 +226,9 @@ export default function ChatWidget() {
|
||||
<span className="absolute bottom-0 right-0 w-3 h-3 bg-green-400 rounded-full border-2 border-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-sm">Dennis's AI Assistant</h3>
|
||||
<h3 className="font-bold text-sm">
|
||||
Dennis's AI Assistant
|
||||
</h3>
|
||||
<p className="text-xs text-white/80">Always online</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -358,7 +360,7 @@ export default function ChatWidget() {
|
||||
{/* Quick Actions */}
|
||||
<div className="flex gap-2 mt-2 overflow-x-auto pb-1 scrollbar-hide">
|
||||
{[
|
||||
"What are Dennis's skills?",
|
||||
"What are Dennis's skills?",
|
||||
"Tell me about his projects",
|
||||
"How can I contact him?",
|
||||
].map((suggestion, index) => (
|
||||
|
||||
@@ -18,14 +18,6 @@ const Hero = () => {
|
||||
{ icon: Rocket, text: "Self-Hosted Infrastructure" },
|
||||
];
|
||||
|
||||
// Smooth scroll configuration
|
||||
const smoothTransition = {
|
||||
type: "spring",
|
||||
damping: 30,
|
||||
stiffness: 50,
|
||||
mass: 1,
|
||||
};
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,7 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion, Variants } from "framer-motion";
|
||||
import {
|
||||
ExternalLink,
|
||||
Github,
|
||||
Calendar,
|
||||
Layers,
|
||||
ArrowRight,
|
||||
} from "lucide-react";
|
||||
import { ExternalLink, Github, Layers, ArrowRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
@@ -65,7 +59,7 @@ const Projects = () => {
|
||||
setProjects(data.projects || []);
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("Error loading projects:", error);
|
||||
}
|
||||
}
|
||||
@@ -104,7 +98,7 @@ const Projects = () => {
|
||||
variants={staggerContainer}
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
||||
>
|
||||
{projects.map((project, index) => (
|
||||
{projects.map((project) => (
|
||||
<motion.div
|
||||
key={project.id}
|
||||
variants={fadeInUp}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef, useCallback, Suspense } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
Suspense,
|
||||
} from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Save,
|
||||
@@ -21,8 +27,8 @@ import {
|
||||
Loader2,
|
||||
ExternalLink,
|
||||
Github,
|
||||
Tag
|
||||
} from 'lucide-react';
|
||||
Tag,
|
||||
} from "lucide-react";
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
@@ -42,7 +48,7 @@ interface Project {
|
||||
|
||||
function EditorPageContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const projectId = searchParams.get('id');
|
||||
const projectId = searchParams.get("id");
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [, setProject] = useState<Project | null>(null);
|
||||
@@ -55,49 +61,51 @@ function EditorPageContent() {
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
content: '',
|
||||
category: 'web',
|
||||
title: "",
|
||||
description: "",
|
||||
content: "",
|
||||
category: "web",
|
||||
tags: [] as string[],
|
||||
featured: false,
|
||||
published: false,
|
||||
github: '',
|
||||
live: '',
|
||||
image: ''
|
||||
github: "",
|
||||
live: "",
|
||||
image: "",
|
||||
});
|
||||
|
||||
const loadProject = useCallback(async (id: string) => {
|
||||
try {
|
||||
const response = await fetch('/api/projects');
|
||||
const response = await fetch("/api/projects");
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const foundProject = data.projects.find((p: Project) => p.id.toString() === id);
|
||||
const foundProject = data.projects.find(
|
||||
(p: Project) => p.id.toString() === id,
|
||||
);
|
||||
|
||||
if (foundProject) {
|
||||
setProject(foundProject);
|
||||
setFormData({
|
||||
title: foundProject.title || '',
|
||||
description: foundProject.description || '',
|
||||
content: foundProject.content || '',
|
||||
category: foundProject.category || 'web',
|
||||
title: foundProject.title || "",
|
||||
description: foundProject.description || "",
|
||||
content: foundProject.content || "",
|
||||
category: foundProject.category || "web",
|
||||
tags: foundProject.tags || [],
|
||||
featured: foundProject.featured || false,
|
||||
published: foundProject.published || false,
|
||||
github: foundProject.github || '',
|
||||
live: foundProject.live || '',
|
||||
image: foundProject.image || ''
|
||||
github: foundProject.github || "",
|
||||
live: foundProject.live || "",
|
||||
image: foundProject.image || "",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Failed to fetch projects:', response.status);
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("Failed to fetch projects:", response.status);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Error loading project:', error);
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("Error loading project:", error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
@@ -107,10 +115,10 @@ function EditorPageContent() {
|
||||
const init = async () => {
|
||||
try {
|
||||
// Check auth
|
||||
const authStatus = sessionStorage.getItem('admin_authenticated');
|
||||
const sessionToken = sessionStorage.getItem('admin_session_token');
|
||||
const authStatus = sessionStorage.getItem("admin_authenticated");
|
||||
const sessionToken = sessionStorage.getItem("admin_session_token");
|
||||
|
||||
if (authStatus === 'true' && sessionToken) {
|
||||
if (authStatus === "true" && sessionToken) {
|
||||
setIsAuthenticated(true);
|
||||
|
||||
// Load project if editing
|
||||
@@ -123,8 +131,8 @@ function EditorPageContent() {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Error in init:', error);
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("Error in init:", error);
|
||||
}
|
||||
setIsAuthenticated(false);
|
||||
} finally {
|
||||
@@ -141,17 +149,17 @@ function EditorPageContent() {
|
||||
|
||||
// Validate required fields
|
||||
if (!formData.title.trim()) {
|
||||
alert('Please enter a project title');
|
||||
alert("Please enter a project title");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.description.trim()) {
|
||||
alert('Please enter a project description');
|
||||
alert("Please enter a project description");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = projectId ? `/api/projects/${projectId}` : '/api/projects';
|
||||
const method = projectId ? 'PUT' : 'POST';
|
||||
const url = projectId ? `/api/projects/${projectId}` : "/api/projects";
|
||||
const method = projectId ? "PUT" : "POST";
|
||||
|
||||
// Prepare data for saving - only include fields that exist in the database schema
|
||||
const saveData = {
|
||||
@@ -166,16 +174,16 @@ function EditorPageContent() {
|
||||
published: formData.published,
|
||||
featured: formData.featured,
|
||||
// Add required fields that might be missing
|
||||
date: new Date().toISOString().split('T')[0] // Current date in YYYY-MM-DD format
|
||||
date: new Date().toISOString().split("T")[0], // Current date in YYYY-MM-DD format
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-admin-request': 'true'
|
||||
"Content-Type": "application/json",
|
||||
"x-admin-request": "true",
|
||||
},
|
||||
body: JSON.stringify(saveData)
|
||||
body: JSON.stringify(saveData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -183,77 +191,106 @@ function EditorPageContent() {
|
||||
|
||||
// Update local state with the saved project data
|
||||
setProject(savedProject);
|
||||
setFormData(prev => ({
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
title: savedProject.title || '',
|
||||
description: savedProject.description || '',
|
||||
content: savedProject.content || '',
|
||||
category: savedProject.category || 'web',
|
||||
title: savedProject.title || "",
|
||||
description: savedProject.description || "",
|
||||
content: savedProject.content || "",
|
||||
category: savedProject.category || "web",
|
||||
tags: savedProject.tags || [],
|
||||
featured: savedProject.featured || false,
|
||||
published: savedProject.published || false,
|
||||
github: savedProject.github || '',
|
||||
live: savedProject.live || '',
|
||||
image: savedProject.imageUrl || ''
|
||||
github: savedProject.github || "",
|
||||
live: savedProject.live || "",
|
||||
image: savedProject.imageUrl || "",
|
||||
}));
|
||||
|
||||
// Show success and redirect
|
||||
alert('Project saved successfully!');
|
||||
alert("Project saved successfully!");
|
||||
setTimeout(() => {
|
||||
window.location.href = '/manage';
|
||||
window.location.href = "/manage";
|
||||
}, 1000);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Error saving project:', response.status, errorData);
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("Error saving project:", response.status, errorData);
|
||||
}
|
||||
alert(`Error saving project: ${errorData.error || 'Unknown error'}`);
|
||||
alert(`Error saving project: ${errorData.error || "Unknown error"}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Error saving project:', error);
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("Error saving project:", error);
|
||||
}
|
||||
alert(`Error saving project: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
alert(
|
||||
`Error saving project: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (field: string, value: string | boolean | string[]) => {
|
||||
setFormData(prev => ({
|
||||
const handleInputChange = (
|
||||
field: string,
|
||||
value: string | boolean | string[],
|
||||
) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
[field]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleTagsChange = (tagsString: string) => {
|
||||
const tags = tagsString.split(',').map(tag => tag.trim()).filter(tag => tag);
|
||||
setFormData(prev => ({
|
||||
const tags = tagsString
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.filter((tag) => tag);
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
tags
|
||||
tags,
|
||||
}));
|
||||
};
|
||||
|
||||
// Markdown components for react-markdown with security
|
||||
const markdownComponents = {
|
||||
a: ({ node, ...props }: { node?: unknown; href?: string; children?: React.ReactNode }) => {
|
||||
a: ({
|
||||
node: _node,
|
||||
...props
|
||||
}: {
|
||||
node?: unknown;
|
||||
href?: string;
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
// Validate URLs to prevent javascript: and data: protocols
|
||||
const href = props.href || '';
|
||||
const isSafe = href && !href.startsWith('javascript:') && !href.startsWith('data:');
|
||||
const href = props.href || "";
|
||||
const isSafe =
|
||||
href && !href.startsWith("javascript:") && !href.startsWith("data:");
|
||||
return (
|
||||
<a
|
||||
{...props}
|
||||
href={isSafe ? href : '#'}
|
||||
target={isSafe && href.startsWith('http') ? '_blank' : undefined}
|
||||
rel={isSafe && href.startsWith('http') ? 'noopener noreferrer' : undefined}
|
||||
href={isSafe ? href : "#"}
|
||||
target={isSafe && href.startsWith("http") ? "_blank" : undefined}
|
||||
rel={
|
||||
isSafe && href.startsWith("http")
|
||||
? "noopener noreferrer"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
img: ({ node, ...props }: { node?: unknown; src?: string; alt?: string }) => {
|
||||
img: ({
|
||||
node: _node,
|
||||
...props
|
||||
}: {
|
||||
node?: unknown;
|
||||
src?: string;
|
||||
alt?: string;
|
||||
}) => {
|
||||
// Validate image URLs
|
||||
const src = props.src || '';
|
||||
const isSafe = src && !src.startsWith('javascript:') && !src.startsWith('data:');
|
||||
return isSafe ? <img {...props} src={src} alt={props.alt || ''} /> : null;
|
||||
const src = props.src || "";
|
||||
const isSafe =
|
||||
src && !src.startsWith("javascript:") && !src.startsWith("data:");
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
return isSafe ? <img {...props} src={src} alt={props.alt || ""} /> : null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -266,46 +303,46 @@ function EditorPageContent() {
|
||||
if (!selection || selection.rangeCount === 0) return;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
let newText = '';
|
||||
let newText = "";
|
||||
|
||||
switch (format) {
|
||||
case 'bold':
|
||||
newText = `**${selection.toString() || 'bold text'}**`;
|
||||
case "bold":
|
||||
newText = `**${selection.toString() || "bold text"}**`;
|
||||
break;
|
||||
case 'italic':
|
||||
newText = `*${selection.toString() || 'italic text'}*`;
|
||||
case "italic":
|
||||
newText = `*${selection.toString() || "italic text"}*`;
|
||||
break;
|
||||
case 'code':
|
||||
newText = `\`${selection.toString() || 'code'}\``;
|
||||
case "code":
|
||||
newText = `\`${selection.toString() || "code"}\``;
|
||||
break;
|
||||
case 'h1':
|
||||
newText = `# ${selection.toString() || 'Heading 1'}`;
|
||||
case "h1":
|
||||
newText = `# ${selection.toString() || "Heading 1"}`;
|
||||
break;
|
||||
case 'h2':
|
||||
newText = `## ${selection.toString() || 'Heading 2'}`;
|
||||
case "h2":
|
||||
newText = `## ${selection.toString() || "Heading 2"}`;
|
||||
break;
|
||||
case 'h3':
|
||||
newText = `### ${selection.toString() || 'Heading 3'}`;
|
||||
case "h3":
|
||||
newText = `### ${selection.toString() || "Heading 3"}`;
|
||||
break;
|
||||
case 'list':
|
||||
newText = `- ${selection.toString() || 'List item'}`;
|
||||
case "list":
|
||||
newText = `- ${selection.toString() || "List item"}`;
|
||||
break;
|
||||
case 'orderedList':
|
||||
newText = `1. ${selection.toString() || 'List item'}`;
|
||||
case "orderedList":
|
||||
newText = `1. ${selection.toString() || "List item"}`;
|
||||
break;
|
||||
case 'quote':
|
||||
newText = `> ${selection.toString() || 'Quote'}`;
|
||||
case "quote":
|
||||
newText = `> ${selection.toString() || "Quote"}`;
|
||||
break;
|
||||
case 'link':
|
||||
const url = prompt('Enter URL:');
|
||||
case "link":
|
||||
const url = prompt("Enter URL:");
|
||||
if (url) {
|
||||
newText = `[${selection.toString() || 'link text'}](${url})`;
|
||||
newText = `[${selection.toString() || "link text"}](${url})`;
|
||||
}
|
||||
break;
|
||||
case 'image':
|
||||
const imageUrl = prompt('Enter image URL:');
|
||||
case "image":
|
||||
const imageUrl = prompt("Enter image URL:");
|
||||
if (imageUrl) {
|
||||
newText = ``;
|
||||
newText = ``;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -315,9 +352,9 @@ function EditorPageContent() {
|
||||
range.insertNode(document.createTextNode(newText));
|
||||
|
||||
// Update form data
|
||||
setFormData(prev => ({
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
content: content.textContent || ''
|
||||
content: content.textContent || "",
|
||||
}));
|
||||
}
|
||||
};
|
||||
@@ -336,7 +373,9 @@ function EditorPageContent() {
|
||||
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
||||
className="w-12 h-12 border-3 border-blue-500 border-t-transparent rounded-full mx-auto mb-6"
|
||||
/>
|
||||
<h2 className="text-xl font-semibold gradient-text mb-2">Loading Editor</h2>
|
||||
<h2 className="text-xl font-semibold gradient-text mb-2">
|
||||
Loading Editor
|
||||
</h2>
|
||||
<p className="text-gray-400">Preparing your workspace...</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
@@ -357,11 +396,13 @@ function EditorPageContent() {
|
||||
<X className="w-8 h-8 text-red-400" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold mb-2">Access Denied</h1>
|
||||
<p className="text-white/70 mb-6">You need to be logged in to access the editor.</p>
|
||||
<p className="text-white/70 mb-6">
|
||||
You need to be logged in to access the editor.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => window.location.href = '/manage'}
|
||||
onClick={() => (window.location.href = "/manage")}
|
||||
className="w-full px-6 py-3 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-xl hover:scale-105 transition-all font-medium"
|
||||
>
|
||||
Go to Admin Login
|
||||
@@ -379,7 +420,7 @@ function EditorPageContent() {
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between h-auto sm:h-16 py-4 sm:py-0 gap-4 sm:gap-0">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center space-y-2 sm:space-y-0 sm:space-x-4">
|
||||
<button
|
||||
onClick={() => window.location.href = '/manage'}
|
||||
onClick={() => (window.location.href = "/manage")}
|
||||
className="inline-flex items-center space-x-2 text-blue-400 hover:text-blue-300 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
@@ -388,7 +429,9 @@ function EditorPageContent() {
|
||||
</button>
|
||||
<div className="hidden sm:block h-6 w-px bg-white/20" />
|
||||
<h1 className="text-lg sm:text-xl font-semibold gradient-text truncate max-w-xs sm:max-w-none">
|
||||
{isCreating ? 'Create New Project' : `Edit: ${formData.title || 'Untitled'}`}
|
||||
{isCreating
|
||||
? "Create New Project"
|
||||
: `Edit: ${formData.title || "Untitled"}`}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -397,8 +440,8 @@ function EditorPageContent() {
|
||||
onClick={() => setShowPreview(!showPreview)}
|
||||
className={`flex items-center space-x-2 px-4 py-2 rounded-lg font-medium transition-all duration-200 text-sm ${
|
||||
showPreview
|
||||
? 'bg-blue-600 text-white shadow-lg'
|
||||
: 'bg-gray-800/50 text-gray-300 hover:bg-gray-700/50 hover:text-white'
|
||||
? "bg-blue-600 text-white shadow-lg"
|
||||
: "bg-gray-800/50 text-gray-300 hover:bg-gray-700/50 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
@@ -415,7 +458,7 @@ function EditorPageContent() {
|
||||
) : (
|
||||
<Save className="w-4 h-4" />
|
||||
)}
|
||||
<span>{isSaving ? 'Saving...' : 'Save Project'}</span>
|
||||
<span>{isSaving ? "Saving..." : "Save Project"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -434,7 +477,7 @@ function EditorPageContent() {
|
||||
style={{
|
||||
left: `${Math.random() * 100}%`,
|
||||
animationDelay: `${Math.random() * 20}s`,
|
||||
animationDuration: `${20 + Math.random() * 10}s`
|
||||
animationDuration: `${20 + Math.random() * 10}s`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -450,7 +493,7 @@ function EditorPageContent() {
|
||||
<input
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => handleInputChange('title', e.target.value)}
|
||||
onChange={(e) => handleInputChange("title", e.target.value)}
|
||||
className="w-full text-3xl font-bold form-input-enhanced focus:outline-none p-4 rounded-lg"
|
||||
placeholder="Enter project title..."
|
||||
/>
|
||||
@@ -466,21 +509,21 @@ function EditorPageContent() {
|
||||
<div className="flex flex-wrap items-center gap-1 sm:gap-2">
|
||||
<div className="flex items-center space-x-1 border-r border-white/20 pr-2 sm:pr-3">
|
||||
<button
|
||||
onClick={() => insertFormatting('bold')}
|
||||
onClick={() => insertFormatting("bold")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Bold"
|
||||
>
|
||||
<Bold className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertFormatting('italic')}
|
||||
onClick={() => insertFormatting("italic")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Italic"
|
||||
>
|
||||
<Italic className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertFormatting('code')}
|
||||
onClick={() => insertFormatting("code")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Code"
|
||||
>
|
||||
@@ -490,21 +533,21 @@ function EditorPageContent() {
|
||||
|
||||
<div className="flex items-center space-x-1 border-r border-white/20 pr-2 sm:pr-3">
|
||||
<button
|
||||
onClick={() => insertFormatting('h1')}
|
||||
onClick={() => insertFormatting("h1")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Heading 1"
|
||||
>
|
||||
<Hash className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertFormatting('h2')}
|
||||
onClick={() => insertFormatting("h2")}
|
||||
className="p-2 hover:bg-gray-800/50 rounded-lg transition-all duration-200 text-xs sm:text-sm text-gray-300 hover:text-white hover:scale-105"
|
||||
title="Heading 2"
|
||||
>
|
||||
H2
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertFormatting('h3')}
|
||||
onClick={() => insertFormatting("h3")}
|
||||
className="p-2 hover:bg-gray-800/50 rounded-lg transition-all duration-200 text-xs sm:text-sm text-gray-300 hover:text-white hover:scale-105"
|
||||
title="Heading 3"
|
||||
>
|
||||
@@ -514,21 +557,21 @@ function EditorPageContent() {
|
||||
|
||||
<div className="flex items-center space-x-1 border-r border-white/20 pr-2 sm:pr-3">
|
||||
<button
|
||||
onClick={() => insertFormatting('list')}
|
||||
onClick={() => insertFormatting("list")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Bullet List"
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertFormatting('orderedList')}
|
||||
onClick={() => insertFormatting("orderedList")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Numbered List"
|
||||
>
|
||||
<ListOrdered className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertFormatting('quote')}
|
||||
onClick={() => insertFormatting("quote")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Quote"
|
||||
>
|
||||
@@ -538,14 +581,14 @@ function EditorPageContent() {
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
<button
|
||||
onClick={() => insertFormatting('link')}
|
||||
onClick={() => insertFormatting("link")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Link"
|
||||
>
|
||||
<Link className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertFormatting('image')}
|
||||
onClick={() => insertFormatting("image")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Image"
|
||||
>
|
||||
@@ -563,18 +606,20 @@ function EditorPageContent() {
|
||||
transition={{ delay: 0.2 }}
|
||||
className="glass-card p-6 rounded-2xl"
|
||||
>
|
||||
<h3 className="text-lg font-semibold gradient-text mb-4">Content</h3>
|
||||
<h3 className="text-lg font-semibold gradient-text mb-4">
|
||||
Content
|
||||
</h3>
|
||||
<div
|
||||
ref={contentRef}
|
||||
contentEditable
|
||||
className="editor-content-editable w-full min-h-[400px] p-6 form-input-enhanced rounded-lg focus:outline-none leading-relaxed"
|
||||
style={{ whiteSpace: 'pre-wrap' }}
|
||||
style={{ whiteSpace: "pre-wrap" }}
|
||||
onInput={(e) => {
|
||||
const target = e.target as HTMLDivElement;
|
||||
setIsTyping(true);
|
||||
setFormData(prev => ({
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
content: target.textContent || ''
|
||||
content: target.textContent || "",
|
||||
}));
|
||||
}}
|
||||
onBlur={() => {
|
||||
@@ -586,7 +631,8 @@ function EditorPageContent() {
|
||||
{!isTyping ? formData.content : undefined}
|
||||
</div>
|
||||
<p className="text-xs text-white/50 mt-2">
|
||||
Supports Markdown formatting. Use the toolbar above or type directly.
|
||||
Supports Markdown formatting. Use the toolbar above or type
|
||||
directly.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
@@ -597,10 +643,14 @@ function EditorPageContent() {
|
||||
transition={{ delay: 0.3 }}
|
||||
className="glass-card p-6 rounded-2xl"
|
||||
>
|
||||
<h3 className="text-lg font-semibold gradient-text mb-4">Description</h3>
|
||||
<h3 className="text-lg font-semibold gradient-text mb-4">
|
||||
Description
|
||||
</h3>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => handleInputChange('description', e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("description", e.target.value)
|
||||
}
|
||||
rows={4}
|
||||
className="w-full px-4 py-3 form-input-enhanced rounded-lg focus:outline-none resize-none"
|
||||
placeholder="Brief description of your project..."
|
||||
@@ -617,7 +667,9 @@ function EditorPageContent() {
|
||||
transition={{ delay: 0.4 }}
|
||||
className="glass-card p-6 rounded-2xl"
|
||||
>
|
||||
<h3 className="text-lg font-semibold gradient-text mb-4">Settings</h3>
|
||||
<h3 className="text-lg font-semibold gradient-text mb-4">
|
||||
Settings
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
@@ -627,7 +679,9 @@ function EditorPageContent() {
|
||||
<div className="custom-select">
|
||||
<select
|
||||
value={formData.category}
|
||||
onChange={(e) => handleInputChange('category', e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("category", e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="web">Web Development</option>
|
||||
<option value="mobile">Mobile Development</option>
|
||||
@@ -639,14 +693,13 @@ function EditorPageContent() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-2">
|
||||
Tags
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.tags.join(', ')}
|
||||
value={formData.tags.join(", ")}
|
||||
onChange={(e) => handleTagsChange(e.target.value)}
|
||||
className="w-full px-3 py-2 form-input-enhanced rounded-lg focus:outline-none"
|
||||
placeholder="React, TypeScript, Next.js"
|
||||
@@ -662,7 +715,9 @@ function EditorPageContent() {
|
||||
transition={{ delay: 0.5 }}
|
||||
className="glass-card p-6 rounded-2xl"
|
||||
>
|
||||
<h3 className="text-lg font-semibold gradient-text mb-4">Links</h3>
|
||||
<h3 className="text-lg font-semibold gradient-text mb-4">
|
||||
Links
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
@@ -672,7 +727,9 @@ function EditorPageContent() {
|
||||
<input
|
||||
type="url"
|
||||
value={formData.github}
|
||||
onChange={(e) => handleInputChange('github', e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("github", e.target.value)
|
||||
}
|
||||
className="w-full px-3 py-2 form-input-enhanced rounded-lg focus:outline-none"
|
||||
placeholder="https://github.com/username/repo"
|
||||
/>
|
||||
@@ -685,7 +742,7 @@ function EditorPageContent() {
|
||||
<input
|
||||
type="url"
|
||||
value={formData.live}
|
||||
onChange={(e) => handleInputChange('live', e.target.value)}
|
||||
onChange={(e) => handleInputChange("live", e.target.value)}
|
||||
className="w-full px-3 py-2 form-input-enhanced rounded-lg focus:outline-none"
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
@@ -700,14 +757,18 @@ function EditorPageContent() {
|
||||
transition={{ delay: 0.6 }}
|
||||
className="glass-card p-6 rounded-2xl"
|
||||
>
|
||||
<h3 className="text-lg font-semibold gradient-text mb-4">Publish</h3>
|
||||
<h3 className="text-lg font-semibold gradient-text mb-4">
|
||||
Publish
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.featured}
|
||||
onChange={(e) => handleInputChange('featured', e.target.checked)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("featured", e.target.checked)
|
||||
}
|
||||
className="w-4 h-4 text-blue-500 bg-gray-900/80 border-gray-600/50 rounded focus:ring-blue-500 focus:ring-2"
|
||||
/>
|
||||
<span className="text-white">Featured Project</span>
|
||||
@@ -717,7 +778,9 @@ function EditorPageContent() {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.published}
|
||||
onChange={(e) => handleInputChange('published', e.target.checked)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("published", e.target.checked)
|
||||
}
|
||||
className="w-4 h-4 text-blue-500 bg-gray-900/80 border-gray-600/50 rounded focus:ring-blue-500 focus:ring-2"
|
||||
/>
|
||||
<span className="text-white">Published</span>
|
||||
@@ -725,10 +788,14 @@ function EditorPageContent() {
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-4 border-t border-white/20">
|
||||
<h4 className="text-sm font-medium text-white/70 mb-2">Preview</h4>
|
||||
<h4 className="text-sm font-medium text-white/70 mb-2">
|
||||
Preview
|
||||
</h4>
|
||||
<div className="text-xs text-white/50 space-y-1">
|
||||
<p>Status: {formData.published ? 'Published' : 'Draft'}</p>
|
||||
{formData.featured && <p className="text-blue-400">⭐ Featured</p>}
|
||||
<p>Status: {formData.published ? "Published" : "Draft"}</p>
|
||||
{formData.featured && (
|
||||
<p className="text-blue-400">⭐ Featured</p>
|
||||
)}
|
||||
<p>Category: {formData.category}</p>
|
||||
<p>Tags: {formData.tags.length} tags</p>
|
||||
</div>
|
||||
@@ -756,7 +823,9 @@ function EditorPageContent() {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold gradient-text">Project Preview</h2>
|
||||
<h2 className="text-2xl font-bold gradient-text">
|
||||
Project Preview
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowPreview(false)}
|
||||
className="p-2 rounded-lg"
|
||||
@@ -770,10 +839,10 @@ function EditorPageContent() {
|
||||
{/* Project Header */}
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold gradient-text mb-4">
|
||||
{formData.title || 'Untitled Project'}
|
||||
{formData.title || "Untitled Project"}
|
||||
</h1>
|
||||
<p className="text-xl text-gray-400 mb-6">
|
||||
{formData.description || 'No description provided'}
|
||||
{formData.description || "No description provided"}
|
||||
</p>
|
||||
|
||||
{/* Project Meta */}
|
||||
@@ -784,7 +853,9 @@ function EditorPageContent() {
|
||||
</div>
|
||||
{formData.featured && (
|
||||
<div className="flex items-center space-x-2 text-blue-400">
|
||||
<span className="text-sm font-semibold">⭐ Featured</span>
|
||||
<span className="text-sm font-semibold">
|
||||
⭐ Featured
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -804,7 +875,8 @@ function EditorPageContent() {
|
||||
)}
|
||||
|
||||
{/* Links */}
|
||||
{((formData.github && formData.github.trim()) || (formData.live && formData.live.trim())) && (
|
||||
{((formData.github && formData.github.trim()) ||
|
||||
(formData.live && formData.live.trim())) && (
|
||||
<div className="flex justify-center space-x-4 mb-8">
|
||||
{formData.github && formData.github.trim() && (
|
||||
<a
|
||||
@@ -835,7 +907,9 @@ function EditorPageContent() {
|
||||
{/* Content Preview */}
|
||||
{formData.content && (
|
||||
<div className="border-t border-white/10 pt-6">
|
||||
<h3 className="text-xl font-semibold gradient-text mb-4">Content</h3>
|
||||
<h3 className="text-xl font-semibold gradient-text mb-4">
|
||||
Content
|
||||
</h3>
|
||||
<div className="prose prose-invert max-w-none">
|
||||
<div className="markdown text-gray-300 leading-relaxed">
|
||||
<ReactMarkdown components={markdownComponents}>
|
||||
@@ -850,12 +924,14 @@ function EditorPageContent() {
|
||||
<div className="border-t border-white/10 pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full text-sm font-medium ${
|
||||
formData.published
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: 'bg-yellow-500/20 text-yellow-400'
|
||||
}`}>
|
||||
{formData.published ? 'Published' : 'Draft'}
|
||||
? "bg-green-500/20 text-green-400"
|
||||
: "bg-yellow-500/20 text-yellow-400"
|
||||
}`}
|
||||
>
|
||||
{formData.published ? "Published" : "Draft"}
|
||||
</span>
|
||||
{formData.featured && (
|
||||
<span className="px-3 py-1 bg-blue-500/20 text-blue-400 rounded-full text-sm font-medium">
|
||||
@@ -879,9 +955,13 @@ function EditorPageContent() {
|
||||
|
||||
export default function EditorPage() {
|
||||
return (
|
||||
<Suspense fallback={<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
||||
<div className="text-white">Loading editor...</div>
|
||||
</div>}>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<EditorPageContent />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -1,45 +1,56 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;
|
||||
const apiUrl = `${baseUrl}/api/sitemap`; // Verwende die vollständige URL zur API
|
||||
|
||||
// In test runs, allow returning a mocked sitemap explicitly
|
||||
if (process.env.NODE_ENV === 'test' && process.env.GHOST_MOCK_SITEMAP) {
|
||||
if (process.env.NODE_ENV === "test" && process.env.GHOST_MOCK_SITEMAP) {
|
||||
// For tests return a simple object so tests can inspect `.body`
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return { body: process.env.GHOST_MOCK_SITEMAP, headers: { "Content-Type": "application/xml" } } as any;
|
||||
if (process.env.NODE_ENV === "test") {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
return {
|
||||
body: process.env.GHOST_MOCK_SITEMAP,
|
||||
headers: { "Content-Type": "application/xml" },
|
||||
} as any;
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
}
|
||||
return new NextResponse(process.env.GHOST_MOCK_SITEMAP, { headers: { "Content-Type": "application/xml" } });
|
||||
return new NextResponse(process.env.GHOST_MOCK_SITEMAP, {
|
||||
headers: { "Content-Type": "application/xml" },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Holt die Sitemap-Daten von der API
|
||||
// Try global fetch first, then fall back to node-fetch
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
let res: any;
|
||||
try {
|
||||
if (typeof (globalThis as any).fetch === 'function') {
|
||||
if (typeof (globalThis as any).fetch === "function") {
|
||||
res = await (globalThis as any).fetch(apiUrl);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
res = undefined;
|
||||
}
|
||||
|
||||
if (!res || typeof res.ok === 'undefined' || !res.ok) {
|
||||
if (!res || typeof res.ok === "undefined" || !res.ok) {
|
||||
try {
|
||||
const mod = await import('node-fetch');
|
||||
const mod = await import("node-fetch");
|
||||
const nodeFetch = (mod as any).default ?? mod;
|
||||
res = await nodeFetch(apiUrl);
|
||||
res = await (nodeFetch as any)(apiUrl);
|
||||
} catch (err) {
|
||||
console.error('Error fetching sitemap:', err);
|
||||
console.error("Error fetching sitemap:", err);
|
||||
return new NextResponse("Error fetching sitemap", { status: 500 });
|
||||
}
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
if (!res || !res.ok) {
|
||||
console.error(`Failed to fetch sitemap: ${res?.statusText ?? 'no response'}`);
|
||||
console.error(
|
||||
`Failed to fetch sitemap: ${res?.statusText ?? "no response"}`,
|
||||
);
|
||||
return new NextResponse("Failed to fetch sitemap", { status: 500 });
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ export default class ErrorBoundary extends React.Component<
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: any) {
|
||||
static getDerivedStateFromError(_error: unknown) {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error: any, errorInfo: any) {
|
||||
componentDidCatch(error: unknown, errorInfo: React.ErrorInfo) {
|
||||
console.error("ErrorBoundary caught an error:", error, errorInfo);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,29 @@ const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [{
|
||||
ignores: ["node_modules/**", ".next/**", "out/**", "build/**", "next-env.d.ts"]
|
||||
}, ...compat.extends("next/core-web-vitals", "next/typescript")];
|
||||
const eslintConfig = [
|
||||
{
|
||||
ignores: [
|
||||
"node_modules/**",
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
],
|
||||
},
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
{
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
|
||||
@@ -31,28 +31,43 @@ jest.mock("next/navigation", () => ({
|
||||
|
||||
// Mock next/link
|
||||
jest.mock("next/link", () => {
|
||||
return function Link({ children, href }: any) {
|
||||
return function Link({
|
||||
children,
|
||||
href,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
}) {
|
||||
return React.createElement("a", { href }, children);
|
||||
};
|
||||
});
|
||||
|
||||
// Mock next/image
|
||||
jest.mock("next/image", () => {
|
||||
return function Image({ src, alt, ...props }: any) {
|
||||
// eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text
|
||||
return function Image({
|
||||
src,
|
||||
alt,
|
||||
...props
|
||||
}: React.ImgHTMLAttributes<HTMLImageElement>) {
|
||||
return React.createElement("img", { src, alt, ...props });
|
||||
};
|
||||
});
|
||||
|
||||
// Mock react-responsive-masonry if it's used
|
||||
jest.mock("react-responsive-masonry", () => {
|
||||
const MasonryComponent = function Masonry({ children }: any) {
|
||||
const MasonryComponent = function Masonry({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return React.createElement("div", { "data-testid": "masonry" }, children);
|
||||
};
|
||||
|
||||
const ResponsiveMasonryComponent = function ResponsiveMasonry({
|
||||
children,
|
||||
}: any) {
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return React.createElement(
|
||||
"div",
|
||||
{ "data-testid": "responsive-masonry" },
|
||||
|
||||
81
lib/redis.ts
81
lib/redis.ts
@@ -1,25 +1,40 @@
|
||||
import { createClient } from 'redis';
|
||||
import { createClient } from "redis";
|
||||
|
||||
let redisClient: ReturnType<typeof createClient> | null = null;
|
||||
let connectionFailed = false; // Track if connection has permanently failed
|
||||
|
||||
interface RedisError {
|
||||
code?: string;
|
||||
message?: string;
|
||||
errors?: RedisError[];
|
||||
cause?: unknown;
|
||||
}
|
||||
|
||||
// Helper to check if error is connection refused
|
||||
const isConnectionRefused = (err: any): boolean => {
|
||||
const isConnectionRefused = (err: unknown): boolean => {
|
||||
if (!err) return false;
|
||||
|
||||
const error = err as RedisError;
|
||||
|
||||
// Check direct properties
|
||||
if (err.code === 'ECONNREFUSED' || err.message?.includes('ECONNREFUSED')) {
|
||||
if (
|
||||
error.code === "ECONNREFUSED" ||
|
||||
error.message?.includes("ECONNREFUSED")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check AggregateError
|
||||
if (err.errors && Array.isArray(err.errors)) {
|
||||
return err.errors.some((e: any) => e?.code === 'ECONNREFUSED' || e?.message?.includes('ECONNREFUSED'));
|
||||
if (error.errors && Array.isArray(error.errors)) {
|
||||
return error.errors.some(
|
||||
(e: RedisError) =>
|
||||
e?.code === "ECONNREFUSED" || e?.message?.includes("ECONNREFUSED"),
|
||||
);
|
||||
}
|
||||
|
||||
// Check nested error
|
||||
if (err.cause) {
|
||||
return isConnectionRefused(err.cause);
|
||||
if (error.cause) {
|
||||
return isConnectionRefused(error.cause);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -50,46 +65,46 @@ export const getRedisClient = async () => {
|
||||
return false;
|
||||
}
|
||||
return false; // Don't reconnect automatically
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
redisClient.on('error', (err: any) => {
|
||||
redisClient.on("error", (err: unknown) => {
|
||||
// Silently handle connection refused errors - Redis is optional
|
||||
if (isConnectionRefused(err)) {
|
||||
connectionFailed = true;
|
||||
return; // Don't log connection refused errors
|
||||
}
|
||||
// Only log non-connection-refused errors
|
||||
console.error('Redis Client Error:', err);
|
||||
console.error("Redis Client Error:", err);
|
||||
});
|
||||
|
||||
redisClient.on('connect', () => {
|
||||
console.log('Redis Client Connected');
|
||||
redisClient.on("connect", () => {
|
||||
console.log("Redis Client Connected");
|
||||
connectionFailed = false; // Reset on successful connection
|
||||
});
|
||||
|
||||
redisClient.on('ready', () => {
|
||||
console.log('Redis Client Ready');
|
||||
redisClient.on("ready", () => {
|
||||
console.log("Redis Client Ready");
|
||||
connectionFailed = false; // Reset on ready
|
||||
});
|
||||
|
||||
redisClient.on('end', () => {
|
||||
console.log('Redis Client Disconnected');
|
||||
redisClient.on("end", () => {
|
||||
console.log("Redis Client Disconnected");
|
||||
});
|
||||
|
||||
await redisClient.connect().catch((err: any) => {
|
||||
await redisClient.connect().catch((err: unknown) => {
|
||||
// Connection failed
|
||||
if (isConnectionRefused(err)) {
|
||||
connectionFailed = true;
|
||||
// Silently handle connection refused - Redis is optional
|
||||
} else {
|
||||
// Only log non-connection-refused errors
|
||||
console.error('Redis connection failed:', err);
|
||||
console.error("Redis connection failed:", err);
|
||||
}
|
||||
redisClient = null;
|
||||
});
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
// If connection fails, set to null
|
||||
if (isConnectionRefused(error)) {
|
||||
connectionFailed = true;
|
||||
@@ -116,7 +131,7 @@ export const cache = {
|
||||
if (!client) return null;
|
||||
const value = await client.get(key);
|
||||
return value ? JSON.parse(value) : null;
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
// Silently fail if Redis is not available
|
||||
return null;
|
||||
}
|
||||
@@ -128,7 +143,7 @@ export const cache = {
|
||||
if (!client) return false;
|
||||
await client.setEx(key, ttlSeconds, JSON.stringify(value));
|
||||
return true;
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
// Silently fail if Redis is not available
|
||||
return false;
|
||||
}
|
||||
@@ -140,7 +155,7 @@ export const cache = {
|
||||
if (!client) return false;
|
||||
await client.del(key);
|
||||
return true;
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
// Silently fail if Redis is not available
|
||||
return false;
|
||||
}
|
||||
@@ -151,7 +166,7 @@ export const cache = {
|
||||
const client = await getRedisClient();
|
||||
if (!client) return false;
|
||||
return await client.exists(key);
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
// Silently fail if Redis is not available
|
||||
return false;
|
||||
}
|
||||
@@ -163,11 +178,11 @@ export const cache = {
|
||||
if (!client) return false;
|
||||
await client.flushAll();
|
||||
return true;
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
// Silently fail if Redis is not available
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Session management
|
||||
@@ -188,7 +203,7 @@ export const session = {
|
||||
|
||||
async destroy(sessionId: string) {
|
||||
return await cache.del(sessionId);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Analytics caching
|
||||
@@ -202,16 +217,16 @@ export const analyticsCache = {
|
||||
},
|
||||
|
||||
async getOverallStats() {
|
||||
return await cache.get('analytics:overall');
|
||||
return await cache.get("analytics:overall");
|
||||
},
|
||||
|
||||
async setOverallStats(stats: unknown, ttlSeconds = 600) {
|
||||
return await cache.set('analytics:overall', stats, ttlSeconds);
|
||||
return await cache.set("analytics:overall", stats, ttlSeconds);
|
||||
},
|
||||
|
||||
async invalidateProject(projectId: number) {
|
||||
await cache.del(`analytics:project:${projectId}`);
|
||||
await cache.del('analytics:overall');
|
||||
await cache.del("analytics:overall");
|
||||
},
|
||||
|
||||
async clearAll() {
|
||||
@@ -219,12 +234,12 @@ export const analyticsCache = {
|
||||
const client = await getRedisClient();
|
||||
if (!client) return;
|
||||
// Clear all analytics-related keys
|
||||
const keys = await client.keys('analytics:*');
|
||||
const keys = await client.keys("analytics:*");
|
||||
if (keys.length > 0) {
|
||||
await client.del(keys);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
// Silently fail if Redis is not available
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const fetch = require('node-fetch');
|
||||
require('dotenv').config({ path: '.env.local' });
|
||||
require('dotenv').config({ path: '.env' });
|
||||
const fetch = require("node-fetch");
|
||||
require("dotenv").config({ path: ".env.local" });
|
||||
require("dotenv").config({ path: ".env" });
|
||||
|
||||
const webhookUrl = process.env.N8N_WEBHOOK_URL || 'https://n8n.dk0.dev';
|
||||
const webhookUrl = process.env.N8N_WEBHOOK_URL || "https://n8n.dk0.dev";
|
||||
const fullUrl = `${webhookUrl}/webhook/chat`;
|
||||
|
||||
console.log(`Testing connection to: ${fullUrl}`);
|
||||
@@ -11,30 +11,30 @@ console.log(`Testing connection to: ${fullUrl}`);
|
||||
async function testConnection() {
|
||||
try {
|
||||
const response = await fetch(fullUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message: "Hello from test script" })
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: "Hello from test script" }),
|
||||
});
|
||||
|
||||
console.log(`Status: ${response.status} ${response.statusText}`);
|
||||
|
||||
if (response.ok) {
|
||||
const text = await response.text();
|
||||
console.log('Response body:', text);
|
||||
console.log("Response body:", text);
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
console.log('Parsed JSON:', json);
|
||||
} catch (e) {
|
||||
console.log('Could not parse response as JSON');
|
||||
console.log("Parsed JSON:", json);
|
||||
} catch (_e) {
|
||||
console.log("Could not parse response as JSON");
|
||||
}
|
||||
} else {
|
||||
console.log('Response headers:', response.headers.raw());
|
||||
console.log("Response headers:", response.headers.raw());
|
||||
const text = await response.text();
|
||||
console.log('Error body:', text);
|
||||
console.log("Error body:", text);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Connection failed:', error.message);
|
||||
if (error.cause) console.error('Cause:', error.cause);
|
||||
console.error("Connection failed:", error.message);
|
||||
if (error.cause) console.error("Cause:", error.cause);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user