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