Compare commits
No commits in common. "4fd257cbd641b04253f5be00e34ea261a2d3d62b" and "d6760d0f840b81ded3d5c694538e35925c26683c" have entirely different histories.
4fd257cbd6
...
d6760d0f84
File diff suppressed because one or more lines are too long
30
.env.example
30
.env.example
@ -68,36 +68,6 @@ AI_EMBEDDINGS_MODEL=mistral-embed
|
|||||||
# User rate limiting (queries per minute)
|
# User rate limiting (queries per minute)
|
||||||
AI_RATE_LIMIT_MAX_REQUESTS=4
|
AI_RATE_LIMIT_MAX_REQUESTS=4
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# 🎥 VIDEO EMBEDDING - PRODUCTION CONFIGURATION
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Enable local caching of Nextcloud videos (highly recommended)
|
|
||||||
VIDEO_CACHE_ENABLED=true
|
|
||||||
|
|
||||||
# Directory for cached videos (ensure it's writable and has sufficient space)
|
|
||||||
# This directory will grow over time as videos are cached permanently
|
|
||||||
VIDEO_CACHE_DIR=./cache/videos
|
|
||||||
|
|
||||||
# Emergency cleanup threshold in MB - videos are cached indefinitely
|
|
||||||
# Only triggers cleanup when approaching this limit to prevent disk full
|
|
||||||
# Recommended: 2000MB (2GB) for small deployments, 5000MB+ for larger ones
|
|
||||||
VIDEO_CACHE_MAX_SIZE=2000
|
|
||||||
|
|
||||||
# Maximum individual video file size for caching in MB
|
|
||||||
# Videos larger than this will stream directly without caching
|
|
||||||
VIDEO_MAX_SIZE=200
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# CACHING BEHAVIOR
|
|
||||||
# ============================================================================
|
|
||||||
# - Videos downloaded once, cached permanently
|
|
||||||
# - No time-based expiration
|
|
||||||
# - Dramatically improves loading times after first download
|
|
||||||
# - Emergency cleanup only when approaching disk space limit
|
|
||||||
# - Perfect for manually curated forensics training content
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# 🎛️ PERFORMANCE TUNING - SENSIBLE DEFAULTS PROVIDED
|
# 🎛️ PERFORMANCE TUNING - SENSIBLE DEFAULTS PROVIDED
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
import node from '@astrojs/node';
|
import node from '@astrojs/node';
|
||||||
import { remarkVideoPlugin } from './src/utils/remarkVideoPlugin.ts';
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
output: 'server',
|
output: 'server',
|
||||||
@ -8,13 +7,6 @@ export default defineConfig({
|
|||||||
mode: 'standalone'
|
mode: 'standalone'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
markdown: {
|
|
||||||
remarkPlugins: [
|
|
||||||
remarkVideoPlugin
|
|
||||||
],
|
|
||||||
extendDefaultPlugins: true
|
|
||||||
},
|
|
||||||
|
|
||||||
build: {
|
build: {
|
||||||
assets: '_astro'
|
assets: '_astro'
|
||||||
},
|
},
|
||||||
|
@ -11,8 +11,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/node": "^9.3.0",
|
"@astrojs/node": "^9.3.0",
|
||||||
"@aws-sdk/client-s3": "^3.864.0",
|
|
||||||
"@aws-sdk/s3-request-presigner": "^3.864.0",
|
|
||||||
"astro": "^5.12.3",
|
"astro": "^5.12.3",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
|
@ -193,6 +193,7 @@ domains.forEach((domain: any) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script define:vars={{ toolsData: tools, domainAgnosticSoftware, domainAgnosticTools }}>
|
<script define:vars={{ toolsData: tools, domainAgnosticSoftware, domainAgnosticTools }}>
|
||||||
|
// Ensure isToolHosted is available
|
||||||
if (!window.isToolHosted) {
|
if (!window.isToolHosted) {
|
||||||
window.isToolHosted = function(tool) {
|
window.isToolHosted = function(tool) {
|
||||||
return tool.projectUrl !== undefined &&
|
return tool.projectUrl !== undefined &&
|
||||||
@ -764,12 +765,14 @@ domains.forEach((domain: any) => {
|
|||||||
hideToolDetails('both');
|
hideToolDetails('both');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register all functions globally
|
||||||
window.showToolDetails = showToolDetails;
|
window.showToolDetails = showToolDetails;
|
||||||
window.hideToolDetails = hideToolDetails;
|
window.hideToolDetails = hideToolDetails;
|
||||||
window.hideAllToolDetails = hideAllToolDetails;
|
window.hideAllToolDetails = hideAllToolDetails;
|
||||||
window.toggleDomainAgnosticSection = toggleDomainAgnosticSection;
|
window.toggleDomainAgnosticSection = toggleDomainAgnosticSection;
|
||||||
window.showShareDialog = showShareDialog;
|
window.showShareDialog = showShareDialog;
|
||||||
|
|
||||||
|
// Register matrix-prefixed versions for delegation
|
||||||
window.matrixShowToolDetails = showToolDetails;
|
window.matrixShowToolDetails = showToolDetails;
|
||||||
window.matrixHideToolDetails = hideToolDetails;
|
window.matrixHideToolDetails = hideToolDetails;
|
||||||
|
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
---
|
|
||||||
// src/components/Video.astro - SIMPLE responsive video component
|
|
||||||
export interface Props {
|
|
||||||
src: string;
|
|
||||||
title?: string;
|
|
||||||
controls?: boolean;
|
|
||||||
autoplay?: boolean;
|
|
||||||
muted?: boolean;
|
|
||||||
loop?: boolean;
|
|
||||||
aspectRatio?: '16:9' | '4:3' | '1:1';
|
|
||||||
preload?: 'none' | 'metadata' | 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
src,
|
|
||||||
title = 'Video',
|
|
||||||
controls = true,
|
|
||||||
autoplay = false,
|
|
||||||
muted = false,
|
|
||||||
loop = false,
|
|
||||||
aspectRatio = '16:9',
|
|
||||||
preload = 'metadata'
|
|
||||||
} = Astro.props;
|
|
||||||
|
|
||||||
const aspectClass = `aspect-${aspectRatio.replace(':', '-')}`;
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class={`video-container ${aspectClass}`}>
|
|
||||||
<video
|
|
||||||
src={src}
|
|
||||||
controls={controls}
|
|
||||||
autoplay={autoplay}
|
|
||||||
muted={muted}
|
|
||||||
loop={loop}
|
|
||||||
preload={preload}
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
data-video-title={title}
|
|
||||||
>
|
|
||||||
<p>Your browser does not support the video element.</p>
|
|
||||||
</video>
|
|
||||||
{title !== 'Video' && (
|
|
||||||
<div class="video-metadata">
|
|
||||||
<div class="video-title">{title}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
@ -18,7 +18,6 @@ sections:
|
|||||||
review_status: "published"
|
review_status: "published"
|
||||||
---
|
---
|
||||||
|
|
||||||
<video src="https://cloud.cc24.dev/s/ZmPK86M86fWyGQk" controls title="Training Video"></video>
|
|
||||||
> **⚠️ Hinweis**: Dies ist ein vorläufiger, KI-generierter Knowledgebase-Eintrag. Wir freuen uns über Verbesserungen und Ergänzungen durch die Community!
|
> **⚠️ Hinweis**: Dies ist ein vorläufiger, KI-generierter Knowledgebase-Eintrag. Wir freuen uns über Verbesserungen und Ergänzungen durch die Community!
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,8 +18,6 @@ sections:
|
|||||||
advanced_topics: true
|
advanced_topics: true
|
||||||
review_status: "published"
|
review_status: "published"
|
||||||
---
|
---
|
||||||
<video src="https://cloud.cc24.dev/s/ZmPK86M86fWyGQk" controls title="Training Video"></video>
|
|
||||||
|
|
||||||
|
|
||||||
> **⚠️ Hinweis**: Dies ist ein vorläufiger, KI-generierter Knowledgebase-Eintrag. Wir freuen uns über Verbesserungen und Ergänzungen durch die Community!
|
> **⚠️ Hinweis**: Dies ist ein vorläufiger, KI-generierter Knowledgebase-Eintrag. Wir freuen uns über Verbesserungen und Ergänzungen durch die Community!
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load utility functions:', error);
|
console.error('Failed to load utility functions:', error);
|
||||||
|
|
||||||
|
// Provide fallback implementations
|
||||||
(window as any).createToolSlug = (toolName: string) => {
|
(window as any).createToolSlug = (toolName: string) => {
|
||||||
if (!toolName || typeof toolName !== 'string') return '';
|
if (!toolName || typeof toolName !== 'string') return '';
|
||||||
return toolName.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
return toolName.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
||||||
@ -118,6 +119,7 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
|||||||
(window as any).prioritizeSearchResults = prioritizeSearchResults;
|
(window as any).prioritizeSearchResults = prioritizeSearchResults;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
// CRITICAL: Load utility functions FIRST before any URL handling
|
||||||
await loadUtilityFunctions();
|
await loadUtilityFunctions();
|
||||||
|
|
||||||
const THEME_KEY = 'dfir-theme';
|
const THEME_KEY = 'dfir-theme';
|
||||||
@ -171,32 +173,33 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
|||||||
getStoredTheme
|
getStoredTheme
|
||||||
};
|
};
|
||||||
|
|
||||||
(window as any).showToolDetails = function(toolName: string, modalType: string = 'primary') {
|
(window as any).showToolDetails = function(toolName: string, modalType: string = 'primary') {
|
||||||
let attempts = 0;
|
|
||||||
const maxAttempts = 50;
|
|
||||||
|
|
||||||
const tryDelegate = () => {
|
let attempts = 0;
|
||||||
const matrixShowToolDetails = (window as any).matrixShowToolDetails;
|
const maxAttempts = 50;
|
||||||
|
|
||||||
if (matrixShowToolDetails && typeof matrixShowToolDetails === 'function') {
|
const tryDelegate = () => {
|
||||||
return matrixShowToolDetails(toolName, modalType);
|
const matrixShowToolDetails = (window as any).matrixShowToolDetails;
|
||||||
}
|
|
||||||
|
|
||||||
const directShowToolDetails = (window as any).directShowToolDetails;
|
if (matrixShowToolDetails && typeof matrixShowToolDetails === 'function') {
|
||||||
if (directShowToolDetails && typeof directShowToolDetails === 'function') {
|
return matrixShowToolDetails(toolName, modalType);
|
||||||
return directShowToolDetails(toolName, modalType);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
attempts++;
|
const directShowToolDetails = (window as any).directShowToolDetails;
|
||||||
if (attempts < maxAttempts) {
|
if (directShowToolDetails && typeof directShowToolDetails === 'function') {
|
||||||
setTimeout(tryDelegate, 100);
|
return directShowToolDetails(toolName, modalType);
|
||||||
} else {
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
tryDelegate();
|
attempts++;
|
||||||
|
if (attempts < maxAttempts) {
|
||||||
|
setTimeout(tryDelegate, 100);
|
||||||
|
} else {
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tryDelegate();
|
||||||
|
};
|
||||||
|
|
||||||
(window as any).hideToolDetails = function(modalType: string = 'both') {
|
(window as any).hideToolDetails = function(modalType: string = 'both') {
|
||||||
const matrixHideToolDetails = (window as any).matrixHideToolDetails;
|
const matrixHideToolDetails = (window as any).matrixHideToolDetails;
|
||||||
if (matrixHideToolDetails && typeof matrixHideToolDetails === 'function') {
|
if (matrixHideToolDetails && typeof matrixHideToolDetails === 'function') {
|
||||||
@ -226,7 +229,7 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
|||||||
authRequired: data.aiAuthRequired,
|
authRequired: data.aiAuthRequired,
|
||||||
expires: data.expires
|
expires: data.expires
|
||||||
};
|
};
|
||||||
case 'gatedcontent':
|
case 'gatedcontent': // ADD THIS CASE
|
||||||
return {
|
return {
|
||||||
authenticated: data.gatedContentAuthenticated,
|
authenticated: data.gatedContentAuthenticated,
|
||||||
authRequired: data.gatedContentAuthRequired,
|
authRequired: data.gatedContentAuthRequired,
|
||||||
@ -350,36 +353,6 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
|||||||
};
|
};
|
||||||
initAIButton();
|
initAIButton();
|
||||||
});
|
});
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const isFirefox = navigator.userAgent.toLowerCase().includes('firefox') ||
|
|
||||||
navigator.userAgent.toLowerCase().includes('librewolf');
|
|
||||||
|
|
||||||
if (isFirefox) {
|
|
||||||
console.log('[VIDEO] Firefox detected - setting up error recovery');
|
|
||||||
|
|
||||||
document.querySelectorAll('video').forEach(video => {
|
|
||||||
let errorCount = 0;
|
|
||||||
|
|
||||||
video.addEventListener('error', () => {
|
|
||||||
errorCount++;
|
|
||||||
console.log(`[VIDEO] Error ${errorCount} in Firefox for: ${video.getAttribute('data-video-title')}`);
|
|
||||||
|
|
||||||
if (errorCount === 1 && video.src.includes('/download')) {
|
|
||||||
console.log('[VIDEO] Trying /preview URL for Firefox compatibility');
|
|
||||||
video.src = video.src.replace('/download', '/preview');
|
|
||||||
video.load();
|
|
||||||
} else if (errorCount === 1) {
|
|
||||||
console.log('[VIDEO] Video failed to load in Firefox');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
video.addEventListener('loadedmetadata', () => {
|
|
||||||
const title = video.getAttribute('data-video-title') || 'Video';
|
|
||||||
console.log(`[VIDEO] Successfully loaded: ${title}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
// src/pages/api/auth/login.ts
|
|
||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import { generateAuthUrl, generateState, logAuthEvent } from '../../../utils/auth.js';
|
import { generateAuthUrl, generateState, logAuthEvent } from '../../../utils/auth.js';
|
||||||
import { serialize } from 'cookie';
|
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
@ -10,27 +8,14 @@ export const GET: APIRoute = async ({ url, redirect }) => {
|
|||||||
const state = generateState();
|
const state = generateState();
|
||||||
const authUrl = generateAuthUrl(state);
|
const authUrl = generateAuthUrl(state);
|
||||||
|
|
||||||
console.log('[AUTH] Generated auth URL:', authUrl);
|
console.log('Generated auth URL:', authUrl);
|
||||||
|
|
||||||
const returnTo = url.searchParams.get('returnTo') || '/';
|
const returnTo = url.searchParams.get('returnTo') || '/';
|
||||||
|
|
||||||
logAuthEvent('Login initiated', { returnTo, authUrl });
|
logAuthEvent('Login initiated', { returnTo, authUrl });
|
||||||
|
|
||||||
const stateData = JSON.stringify({ state, returnTo });
|
const stateData = JSON.stringify({ state, returnTo });
|
||||||
|
const stateCookie = `auth_state=${encodeURIComponent(stateData)}; HttpOnly; SameSite=Lax; Path=/; Max-Age=600`;
|
||||||
const publicBaseUrl = process.env.PUBLIC_BASE_URL || '';
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
const isSecure = publicBaseUrl.startsWith('https://') || isProduction;
|
|
||||||
|
|
||||||
const stateCookie = serialize('auth_state', stateData, {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: isSecure,
|
|
||||||
sameSite: 'lax',
|
|
||||||
maxAge: 600, // 10 minutes
|
|
||||||
path: '/'
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[AUTH] Setting auth state cookie:', stateCookie.substring(0, 50) + '...');
|
|
||||||
|
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// src/pages/api/auth/process.ts
|
// src/pages/api/auth/process.ts (FIXED - Proper cookie handling)
|
||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import {
|
import {
|
||||||
verifyAuthState,
|
verifyAuthState,
|
||||||
@ -7,7 +7,7 @@ import {
|
|||||||
createSessionWithCookie,
|
createSessionWithCookie,
|
||||||
logAuthEvent
|
logAuthEvent
|
||||||
} from '../../../utils/auth.js';
|
} from '../../../utils/auth.js';
|
||||||
import { apiError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
|
import { apiError, apiSpecial, apiWithHeaders, handleAPIRequest } from '../../../utils/api.js';
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
@ -30,15 +30,9 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
|
|
||||||
const stateVerification = verifyAuthState(request, state);
|
const stateVerification = verifyAuthState(request, state);
|
||||||
if (!stateVerification.isValid || !stateVerification.stateData) {
|
if (!stateVerification.isValid || !stateVerification.stateData) {
|
||||||
logAuthEvent('State verification failed', {
|
|
||||||
error: stateVerification.error,
|
|
||||||
hasStateData: !!stateVerification.stateData
|
|
||||||
});
|
|
||||||
return apiError.badRequest(stateVerification.error || 'Invalid state parameter');
|
return apiError.badRequest(stateVerification.error || 'Invalid state parameter');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[AUTH] State verification successful, exchanging code for tokens');
|
|
||||||
|
|
||||||
const tokens = await exchangeCodeForTokens(code);
|
const tokens = await exchangeCodeForTokens(code);
|
||||||
const userInfo = await getUserInfo(tokens.access_token);
|
const userInfo = await getUserInfo(tokens.access_token);
|
||||||
|
|
||||||
@ -49,12 +43,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
email: sessionResult.userEmail
|
email: sessionResult.userEmail
|
||||||
});
|
});
|
||||||
|
|
||||||
const returnUrl = new URL(stateVerification.stateData.returnTo, request.url);
|
|
||||||
returnUrl.searchParams.set('auth', 'success');
|
|
||||||
const redirectUrl = returnUrl.toString();
|
|
||||||
|
|
||||||
console.log('[AUTH] Redirecting to:', redirectUrl);
|
|
||||||
|
|
||||||
const responseHeaders = new Headers();
|
const responseHeaders = new Headers();
|
||||||
responseHeaders.set('Content-Type', 'application/json');
|
responseHeaders.set('Content-Type', 'application/json');
|
||||||
|
|
||||||
@ -63,7 +51,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
|
|
||||||
return new Response(JSON.stringify({
|
return new Response(JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
redirectTo: redirectUrl
|
redirectTo: stateVerification.stateData.returnTo
|
||||||
}), {
|
}), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: responseHeaders
|
headers: responseHeaders
|
||||||
|
@ -9,16 +9,16 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
return await handleAPIRequest(async () => {
|
return await handleAPIRequest(async () => {
|
||||||
const contributionAuth = await withAPIAuth(request, 'contributions');
|
const contributionAuth = await withAPIAuth(request, 'contributions');
|
||||||
const aiAuth = await withAPIAuth(request, 'ai');
|
const aiAuth = await withAPIAuth(request, 'ai');
|
||||||
const gatedContentAuth = await withAPIAuth(request, 'gatedcontent');
|
const gatedContentAuth = await withAPIAuth(request, 'gatedcontent'); // ADDED
|
||||||
|
|
||||||
return apiResponse.success({
|
return apiResponse.success({
|
||||||
authenticated: contributionAuth.authenticated || aiAuth.authenticated || gatedContentAuth.authenticated,
|
authenticated: contributionAuth.authenticated || aiAuth.authenticated || gatedContentAuth.authenticated,
|
||||||
contributionAuthRequired: contributionAuth.authRequired,
|
contributionAuthRequired: contributionAuth.authRequired,
|
||||||
aiAuthRequired: aiAuth.authRequired,
|
aiAuthRequired: aiAuth.authRequired,
|
||||||
gatedContentAuthRequired: gatedContentAuth.authRequired,
|
gatedContentAuthRequired: gatedContentAuth.authRequired, // ADDED
|
||||||
contributionAuthenticated: contributionAuth.authenticated,
|
contributionAuthenticated: contributionAuth.authenticated,
|
||||||
aiAuthenticated: aiAuth.authenticated,
|
aiAuthenticated: aiAuth.authenticated,
|
||||||
gatedContentAuthenticated: gatedContentAuth.authenticated,
|
gatedContentAuthenticated: gatedContentAuth.authenticated, // ADDED
|
||||||
expires: contributionAuth.session?.exp ? new Date(contributionAuth.session.exp * 1000).toISOString() : null
|
expires: contributionAuth.session?.exp ? new Date(contributionAuth.session.exp * 1000).toISOString() : null
|
||||||
});
|
});
|
||||||
}, 'Status check failed');
|
}, 'Status check failed');
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// src/pages/api/contribute/knowledgebase.ts
|
// src/pages/api/contribute/knowledgebase.ts - SIMPLIFIED: Issues only, minimal validation
|
||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import { withAPIAuth } from '../../../utils/auth.js';
|
import { withAPIAuth } from '../../../utils/auth.js';
|
||||||
import { apiResponse, apiError, apiServerError, handleAPIRequest } from '../../../utils/api.js';
|
import { apiResponse, apiError, apiServerError, handleAPIRequest } from '../../../utils/api.js';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// src/pages/api/contribute/tool.ts
|
// src/pages/api/contribute/tool.ts (UPDATED - Using consolidated API responses + related_software)
|
||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import { withAPIAuth } from '../../../utils/auth.js';
|
import { withAPIAuth } from '../../../utils/auth.js';
|
||||||
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
|
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
|
||||||
@ -82,27 +82,31 @@ function sanitizeInput(obj: any): any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function preprocessFormData(body: any): any {
|
function preprocessFormData(body: any): any {
|
||||||
|
// Handle comma-separated strings from autocomplete inputs
|
||||||
if (body.tool) {
|
if (body.tool) {
|
||||||
|
// Handle tags
|
||||||
if (typeof body.tool.tags === 'string') {
|
if (typeof body.tool.tags === 'string') {
|
||||||
body.tool.tags = body.tool.tags.split(',').map((t: string) => t.trim()).filter(Boolean);
|
body.tool.tags = body.tool.tags.split(',').map((t: string) => t.trim()).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle related concepts
|
||||||
if (body.tool.relatedConcepts) {
|
if (body.tool.relatedConcepts) {
|
||||||
if (typeof body.tool.relatedConcepts === 'string') {
|
if (typeof body.tool.relatedConcepts === 'string') {
|
||||||
body.tool.related_concepts = body.tool.relatedConcepts.split(',').map((t: string) => t.trim()).filter(Boolean);
|
body.tool.related_concepts = body.tool.relatedConcepts.split(',').map((t: string) => t.trim()).filter(Boolean);
|
||||||
} else {
|
} else {
|
||||||
body.tool.related_concepts = body.tool.relatedConcepts;
|
body.tool.related_concepts = body.tool.relatedConcepts;
|
||||||
}
|
}
|
||||||
delete body.tool.relatedConcepts;
|
delete body.tool.relatedConcepts; // Remove the original key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle related software
|
||||||
if (body.tool.relatedSoftware) {
|
if (body.tool.relatedSoftware) {
|
||||||
if (typeof body.tool.relatedSoftware === 'string') {
|
if (typeof body.tool.relatedSoftware === 'string') {
|
||||||
body.tool.related_software = body.tool.relatedSoftware.split(',').map((t: string) => t.trim()).filter(Boolean);
|
body.tool.related_software = body.tool.relatedSoftware.split(',').map((t: string) => t.trim()).filter(Boolean);
|
||||||
} else {
|
} else {
|
||||||
body.tool.related_software = body.tool.relatedSoftware;
|
body.tool.related_software = body.tool.relatedSoftware;
|
||||||
}
|
}
|
||||||
delete body.tool.relatedSoftware;
|
delete body.tool.relatedSoftware; // Remove the original key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,11 +142,14 @@ async function validateToolData(tool: any, action: string): Promise<{ valid: boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate related items exist (optional validation - could be enhanced)
|
||||||
if (tool.related_concepts && tool.related_concepts.length > 0) {
|
if (tool.related_concepts && tool.related_concepts.length > 0) {
|
||||||
|
// Could validate that referenced concepts actually exist
|
||||||
console.log('[VALIDATION] Related concepts provided:', tool.related_concepts);
|
console.log('[VALIDATION] Related concepts provided:', tool.related_concepts);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tool.related_software && tool.related_software.length > 0) {
|
if (tool.related_software && tool.related_software.length > 0) {
|
||||||
|
// Could validate that referenced software actually exists
|
||||||
console.log('[VALIDATION] Related software provided:', tool.related_software);
|
console.log('[VALIDATION] Related software provided:', tool.related_software);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- (rest of the handler unchanged) -------------------------- */
|
||||||
const { embeddingsService } = await import('../../../utils/embeddings.js');
|
const { embeddingsService } = await import('../../../utils/embeddings.js');
|
||||||
|
|
||||||
if (!embeddingsService.isEnabled()) {
|
if (!embeddingsService.isEnabled()) {
|
||||||
|
@ -23,6 +23,7 @@ const editToolName = Astro.url.searchParams.get('edit');
|
|||||||
const editTool = editToolName ? existingTools.find(tool => tool.name === editToolName) : null;
|
const editTool = editToolName ? existingTools.find(tool => tool.name === editToolName) : null;
|
||||||
const isEdit = !!editTool;
|
const isEdit = !!editTool;
|
||||||
|
|
||||||
|
// Extract data for autocomplete
|
||||||
const allTags = [...new Set(existingTools.flatMap(tool => tool.tags || []))].sort();
|
const allTags = [...new Set(existingTools.flatMap(tool => tool.tags || []))].sort();
|
||||||
const allSoftwareAndMethods = existingTools
|
const allSoftwareAndMethods = existingTools
|
||||||
.filter(tool => tool.type === 'software' || tool.type === 'method')
|
.filter(tool => tool.type === 'software' || tool.type === 'method')
|
||||||
@ -299,6 +300,7 @@ const allConcepts = existingTools
|
|||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<script define:vars={{ isEdit, editTool, domains, phases, domainAgnosticSoftware, allTags, allSoftwareAndMethods, allConcepts }}>
|
<script define:vars={{ isEdit, editTool, domains, phases, domainAgnosticSoftware, allTags, allSoftwareAndMethods, allConcepts }}>
|
||||||
|
// Consolidated Autocomplete Functionality - inlined to avoid module loading issues
|
||||||
class AutocompleteManager {
|
class AutocompleteManager {
|
||||||
constructor(inputElement, dataSource, options = {}) {
|
constructor(inputElement, dataSource, options = {}) {
|
||||||
this.input = inputElement;
|
this.input = inputElement;
|
||||||
@ -335,6 +337,7 @@ class AutocompleteManager {
|
|||||||
this.dropdown = document.createElement('div');
|
this.dropdown = document.createElement('div');
|
||||||
this.dropdown.className = 'autocomplete-dropdown';
|
this.dropdown.className = 'autocomplete-dropdown';
|
||||||
|
|
||||||
|
// Insert dropdown after input
|
||||||
this.input.parentNode.style.position = 'relative';
|
this.input.parentNode.style.position = 'relative';
|
||||||
this.input.parentNode.insertBefore(this.dropdown, this.input.nextSibling);
|
this.input.parentNode.insertBefore(this.dropdown, this.input.nextSibling);
|
||||||
}
|
}
|
||||||
@ -355,6 +358,7 @@ class AutocompleteManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.input.addEventListener('blur', (e) => {
|
this.input.addEventListener('blur', (e) => {
|
||||||
|
// Delay to allow click events on dropdown items
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.dropdown.contains(document.activeElement)) {
|
if (!this.dropdown.contains(document.activeElement)) {
|
||||||
this.hideDropdown();
|
this.hideDropdown();
|
||||||
@ -446,6 +450,7 @@ class AutocompleteManager {
|
|||||||
})
|
})
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
|
// Bind click events
|
||||||
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
|
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
|
||||||
option.addEventListener('click', () => {
|
option.addEventListener('click', () => {
|
||||||
this.selectItem(this.filteredData[index]);
|
this.selectItem(this.filteredData[index]);
|
||||||
@ -479,6 +484,7 @@ class AutocompleteManager {
|
|||||||
this.hideDropdown();
|
this.hideDropdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger change event
|
||||||
this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
|
this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
|
||||||
detail: { item, text, selectedItems: Array.from(this.selectedItems) }
|
detail: { item, text, selectedItems: Array.from(this.selectedItems) }
|
||||||
}));
|
}));
|
||||||
@ -504,6 +510,7 @@ class AutocompleteManager {
|
|||||||
`)
|
`)
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
|
// Bind remove events
|
||||||
this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => {
|
this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => {
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -629,6 +636,7 @@ class ContributionForm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupAutocomplete() {
|
setupAutocomplete() {
|
||||||
|
// Tags autocomplete
|
||||||
if (this.elements.tagsInput && this.elements.tagsHidden) {
|
if (this.elements.tagsInput && this.elements.tagsHidden) {
|
||||||
const tagsManager = new AutocompleteManager(this.elements.tagsInput, allTags, {
|
const tagsManager = new AutocompleteManager(this.elements.tagsInput, allTags, {
|
||||||
allowMultiple: true,
|
allowMultiple: true,
|
||||||
@ -636,6 +644,7 @@ class ContributionForm {
|
|||||||
placeholder: 'Beginne zu tippen, um Tags hinzuzufügen...'
|
placeholder: 'Beginne zu tippen, um Tags hinzuzufügen...'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set initial values if editing
|
||||||
if (this.editTool?.tags) {
|
if (this.editTool?.tags) {
|
||||||
tagsManager.setSelectedItems(this.editTool.tags);
|
tagsManager.setSelectedItems(this.editTool.tags);
|
||||||
}
|
}
|
||||||
@ -643,6 +652,7 @@ class ContributionForm {
|
|||||||
this.autocompleteManagers.set('tags', tagsManager);
|
this.autocompleteManagers.set('tags', tagsManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Related concepts autocomplete
|
||||||
if (this.elements.relatedConceptsInput && this.elements.relatedConceptsHidden) {
|
if (this.elements.relatedConceptsInput && this.elements.relatedConceptsHidden) {
|
||||||
const conceptsManager = new AutocompleteManager(this.elements.relatedConceptsInput, allConcepts, {
|
const conceptsManager = new AutocompleteManager(this.elements.relatedConceptsInput, allConcepts, {
|
||||||
allowMultiple: true,
|
allowMultiple: true,
|
||||||
@ -650,6 +660,7 @@ class ContributionForm {
|
|||||||
placeholder: 'Beginne zu tippen, um Konzepte zu finden...'
|
placeholder: 'Beginne zu tippen, um Konzepte zu finden...'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set initial values if editing
|
||||||
if (this.editTool?.related_concepts) {
|
if (this.editTool?.related_concepts) {
|
||||||
conceptsManager.setSelectedItems(this.editTool.related_concepts);
|
conceptsManager.setSelectedItems(this.editTool.related_concepts);
|
||||||
}
|
}
|
||||||
@ -657,6 +668,7 @@ class ContributionForm {
|
|||||||
this.autocompleteManagers.set('relatedConcepts', conceptsManager);
|
this.autocompleteManagers.set('relatedConcepts', conceptsManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Related software autocomplete
|
||||||
if (this.elements.relatedSoftwareInput && this.elements.relatedSoftwareHidden) {
|
if (this.elements.relatedSoftwareInput && this.elements.relatedSoftwareHidden) {
|
||||||
const softwareManager = new AutocompleteManager(this.elements.relatedSoftwareInput, allSoftwareAndMethods, {
|
const softwareManager = new AutocompleteManager(this.elements.relatedSoftwareInput, allSoftwareAndMethods, {
|
||||||
allowMultiple: true,
|
allowMultiple: true,
|
||||||
@ -664,6 +676,7 @@ class ContributionForm {
|
|||||||
placeholder: 'Beginne zu tippen, um Software/Methoden zu finden...'
|
placeholder: 'Beginne zu tippen, um Software/Methoden zu finden...'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set initial values if editing
|
||||||
if (this.editTool?.related_software) {
|
if (this.editTool?.related_software) {
|
||||||
softwareManager.setSelectedItems(this.editTool.related_software);
|
softwareManager.setSelectedItems(this.editTool.related_software);
|
||||||
}
|
}
|
||||||
@ -671,6 +684,7 @@ class ContributionForm {
|
|||||||
this.autocompleteManagers.set('relatedSoftware', softwareManager);
|
this.autocompleteManagers.set('relatedSoftware', softwareManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for autocomplete changes to update YAML preview
|
||||||
Object.values(this.autocompleteManagers).forEach(manager => {
|
Object.values(this.autocompleteManagers).forEach(manager => {
|
||||||
if (manager.input) {
|
if (manager.input) {
|
||||||
manager.input.addEventListener('autocomplete:select', () => {
|
manager.input.addEventListener('autocomplete:select', () => {
|
||||||
@ -712,10 +726,14 @@ class ContributionForm {
|
|||||||
updateFieldVisibility() {
|
updateFieldVisibility() {
|
||||||
const type = this.elements.typeSelect.value;
|
const type = this.elements.typeSelect.value;
|
||||||
|
|
||||||
|
// Only hide/show software-specific fields (platforms, license)
|
||||||
|
// Relations should always be visible since all tool types can have relationships
|
||||||
this.elements.softwareFields.style.display = type === 'software' ? 'block' : 'none';
|
this.elements.softwareFields.style.display = type === 'software' ? 'block' : 'none';
|
||||||
|
|
||||||
|
// Always show relations - all tool types can have relationships
|
||||||
this.elements.relationsFields.style.display = 'block';
|
this.elements.relationsFields.style.display = 'block';
|
||||||
|
|
||||||
|
// Only mark platform/license as required for software
|
||||||
if (this.elements.platformsRequired) {
|
if (this.elements.platformsRequired) {
|
||||||
this.elements.platformsRequired.style.display = type === 'software' ? 'inline' : 'none';
|
this.elements.platformsRequired.style.display = type === 'software' ? 'inline' : 'none';
|
||||||
}
|
}
|
||||||
@ -723,6 +741,7 @@ class ContributionForm {
|
|||||||
this.elements.licenseRequired.style.display = type === 'software' ? 'inline' : 'none';
|
this.elements.licenseRequired.style.display = type === 'software' ? 'inline' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always show both relation sections - let users decide what's relevant
|
||||||
const conceptsSection = document.getElementById('related-concepts-section');
|
const conceptsSection = document.getElementById('related-concepts-section');
|
||||||
const softwareSection = document.getElementById('related-software-section');
|
const softwareSection = document.getElementById('related-software-section');
|
||||||
if (conceptsSection) conceptsSection.style.display = 'block';
|
if (conceptsSection) conceptsSection.style.display = 'block';
|
||||||
@ -787,16 +806,19 @@ class ContributionForm {
|
|||||||
tool.knowledgebase = true;
|
tool.knowledgebase = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle tags from autocomplete
|
||||||
const tagsValue = this.elements.tagsHidden?.value || '';
|
const tagsValue = this.elements.tagsHidden?.value || '';
|
||||||
if (tagsValue) {
|
if (tagsValue) {
|
||||||
tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean);
|
tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle related concepts from autocomplete
|
||||||
const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || '';
|
const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || '';
|
||||||
if (relatedConceptsValue) {
|
if (relatedConceptsValue) {
|
||||||
tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean);
|
tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle related software from autocomplete
|
||||||
const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || '';
|
const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || '';
|
||||||
if (relatedSoftwareValue) {
|
if (relatedSoftwareValue) {
|
||||||
tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean);
|
tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||||
@ -961,16 +983,19 @@ class ContributionForm {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle tags from autocomplete
|
||||||
const tagsValue = this.elements.tagsHidden?.value || '';
|
const tagsValue = this.elements.tagsHidden?.value || '';
|
||||||
if (tagsValue) {
|
if (tagsValue) {
|
||||||
submission.tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean);
|
submission.tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle related concepts from autocomplete
|
||||||
const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || '';
|
const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || '';
|
||||||
if (relatedConceptsValue) {
|
if (relatedConceptsValue) {
|
||||||
submission.tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean);
|
submission.tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle related software from autocomplete
|
||||||
const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || '';
|
const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || '';
|
||||||
if (relatedSoftwareValue) {
|
if (relatedSoftwareValue) {
|
||||||
submission.tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean);
|
submission.tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||||
@ -1047,6 +1072,7 @@ class ContributionForm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
// Clean up autocomplete managers
|
||||||
this.autocompleteManagers.forEach(manager => {
|
this.autocompleteManagers.forEach(manager => {
|
||||||
manager.destroy();
|
manager.destroy();
|
||||||
});
|
});
|
||||||
|
@ -686,6 +686,8 @@ if (aiAuthRequired) {
|
|||||||
window.switchToAIView = () => switchToView('ai');
|
window.switchToAIView = () => switchToView('ai');
|
||||||
window.switchToView = switchToView;
|
window.switchToView = switchToView;
|
||||||
|
|
||||||
|
// CRITICAL: Handle shared URLs AFTER everything is set up
|
||||||
|
// Increased timeout to ensure all components and utility functions are loaded
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
handleSharedURL();
|
handleSharedURL();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
@ -10,6 +10,7 @@ const allKnowledgebaseEntries = await getCollection('knowledgebase', (entry) =>
|
|||||||
return entry.data.published !== false;
|
return entry.data.published !== false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if gated content authentication is enabled globally
|
||||||
const gatedContentAuthEnabled = isGatedContentAuthRequired();
|
const gatedContentAuthEnabled = isGatedContentAuthRequired();
|
||||||
|
|
||||||
const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
|
const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
|
||||||
@ -26,7 +27,8 @@ const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
|
|||||||
difficulty: entry.data.difficulty,
|
difficulty: entry.data.difficulty,
|
||||||
categories: entry.data.categories || [],
|
categories: entry.data.categories || [],
|
||||||
tags: entry.data.tags || [],
|
tags: entry.data.tags || [],
|
||||||
gated_content: entry.data.gated_content || false,
|
gated_content: entry.data.gated_content || false, // NEW: Include gated content flag
|
||||||
|
|
||||||
tool_name: entry.data.tool_name,
|
tool_name: entry.data.tool_name,
|
||||||
related_tools: entry.data.related_tools || [],
|
related_tools: entry.data.related_tools || [],
|
||||||
associatedTool,
|
associatedTool,
|
||||||
@ -43,6 +45,7 @@ const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
|
|||||||
|
|
||||||
knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
|
knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
|
||||||
|
|
||||||
|
// Count gated vs public articles for statistics
|
||||||
const gatedCount = knowledgebaseEntries.filter(entry => entry.gated_content).length;
|
const gatedCount = knowledgebaseEntries.filter(entry => entry.gated_content).length;
|
||||||
const publicCount = knowledgebaseEntries.length - gatedCount;
|
const publicCount = knowledgebaseEntries.length - gatedCount;
|
||||||
---
|
---
|
||||||
|
@ -21,6 +21,7 @@ export async function getStaticPaths() {
|
|||||||
|
|
||||||
const { entry }: { entry: any } = Astro.props;
|
const { entry }: { entry: any } = Astro.props;
|
||||||
|
|
||||||
|
// Check if this article is gated and if gated content auth is required globally
|
||||||
const isGatedContent = entry.data.gated_content === true;
|
const isGatedContent = entry.data.gated_content === true;
|
||||||
const gatedContentAuthRequired = isGatedContentAuthRequired();
|
const gatedContentAuthRequired = isGatedContentAuthRequired();
|
||||||
const requiresAuth = isGatedContent && gatedContentAuthRequired;
|
const requiresAuth = isGatedContent && gatedContentAuthRequired;
|
||||||
@ -61,28 +62,24 @@ const currentUrl = Astro.url.href;
|
|||||||
<BaseLayout title={entry.data.title} description={entry.data.description}>
|
<BaseLayout title={entry.data.title} description={entry.data.description}>
|
||||||
{requiresAuth && (
|
{requiresAuth && (
|
||||||
<script define:vars={{ requiresAuth, articleTitle: entry.data.title }}>
|
<script define:vars={{ requiresAuth, articleTitle: entry.data.title }}>
|
||||||
|
// Client-side authentication check for gated content
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
if (!requiresAuth) return;
|
if (!requiresAuth) return;
|
||||||
|
|
||||||
console.log('[GATED CONTENT] Checking client-side auth for: ' + articleTitle);
|
console.log('[GATED CONTENT] Checking client-side auth for: ' + articleTitle);
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
// Hide content immediately while checking auth
|
||||||
const authSuccess = urlParams.get('auth') === 'success';
|
|
||||||
|
|
||||||
const contentArea = document.querySelector('.article-content');
|
const contentArea = document.querySelector('.article-content');
|
||||||
const sidebar = document.querySelector('.article-sidebar');
|
const sidebar = document.querySelector('.article-sidebar');
|
||||||
|
|
||||||
|
|
||||||
if (contentArea) {
|
if (contentArea) {
|
||||||
contentArea.style.display = 'none';
|
contentArea.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
// DON'T hide the sidebar container - just prevent TOC generation
|
||||||
if (authSuccess) {
|
//if (sidebar) {
|
||||||
console.log('[GATED CONTENT] Auth success detected, waiting for session...');
|
//sidebar.innerHTML = ''; // Clear any content instead of hiding
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
//}
|
||||||
|
|
||||||
const cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
|
|
||||||
window.history.replaceState({}, document.title, cleanUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/auth/status');
|
const response = await fetch('/api/auth/status');
|
||||||
@ -96,6 +93,7 @@ const currentUrl = Astro.url.href;
|
|||||||
if (authRequired && !isAuthenticated) {
|
if (authRequired && !isAuthenticated) {
|
||||||
console.log('[GATED CONTENT] Access denied - showing auth required message: ' + articleTitle);
|
console.log('[GATED CONTENT] Access denied - showing auth required message: ' + articleTitle);
|
||||||
|
|
||||||
|
// Show authentication required message (no auto-redirect)
|
||||||
if (contentArea) {
|
if (contentArea) {
|
||||||
const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href);
|
const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href);
|
||||||
contentArea.innerHTML = [
|
contentArea.innerHTML = [
|
||||||
@ -123,9 +121,11 @@ const currentUrl = Astro.url.href;
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('[GATED CONTENT] Access granted for: ' + articleTitle);
|
console.log('[GATED CONTENT] Access granted for: ' + articleTitle);
|
||||||
|
// Show content for authenticated users
|
||||||
if (contentArea) {
|
if (contentArea) {
|
||||||
contentArea.style.display = 'block';
|
contentArea.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
// Let TOC generate normally for authenticated users
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (typeof generateTOCContent === 'function') {
|
if (typeof generateTOCContent === 'function') {
|
||||||
generateTOCContent();
|
generateTOCContent();
|
||||||
@ -134,6 +134,7 @@ const currentUrl = Astro.url.href;
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[GATED CONTENT] Auth check failed:', error);
|
console.error('[GATED CONTENT] Auth check failed:', error);
|
||||||
|
// On error, show auth required message
|
||||||
if (requiresAuth && contentArea) {
|
if (requiresAuth && contentArea) {
|
||||||
const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href);
|
const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href);
|
||||||
contentArea.innerHTML = [
|
contentArea.innerHTML = [
|
||||||
@ -401,10 +402,29 @@ const currentUrl = Astro.url.href;
|
|||||||
}
|
}
|
||||||
|
|
||||||
function generateSidebarTOC() {
|
function generateSidebarTOC() {
|
||||||
|
// NEW: Don't generate TOC for gated content that requires auth
|
||||||
if (requiresAuth) {
|
if (requiresAuth) {
|
||||||
|
fetch('/api/auth/status')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(authStatus => {
|
||||||
|
const isAuthenticated = authStatus.gatedContentAuthenticated || false;
|
||||||
|
const authRequired = authStatus.gatedContentAuthRequired || false;
|
||||||
|
|
||||||
|
// Only generate TOC if user is authenticated for gated content
|
||||||
|
if (authRequired && !isAuthenticated) {
|
||||||
|
return; // Don't generate TOC
|
||||||
|
} else {
|
||||||
|
generateTOCContent(); // Generate TOC for authenticated users
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// On error, don't generate TOC for gated content
|
||||||
|
return;
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For non-gated content, generate TOC normally
|
||||||
generateTOCContent();
|
generateTOCContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -510,14 +530,17 @@ const currentUrl = Astro.url.href;
|
|||||||
pre.dataset.copyEnhanced = 'true';
|
pre.dataset.copyEnhanced = 'true';
|
||||||
pre.style.position ||= 'relative';
|
pre.style.position ||= 'relative';
|
||||||
|
|
||||||
|
// Try to find an existing copy button we can reuse
|
||||||
let btn =
|
let btn =
|
||||||
pre.querySelector('.copy-btn') ||
|
pre.querySelector('.copy-btn') || // our class
|
||||||
pre.querySelector('.btn-copy, .copy-button, .code-copy, .copy-code, button[aria-label*="copy" i]');
|
pre.querySelector('.btn-copy, .copy-button, .code-copy, .copy-code, button[aria-label*="copy" i]');
|
||||||
|
|
||||||
|
// If there is an "old" button that is NOT ours, prefer to reuse it by giving it our class.
|
||||||
if (btn && !btn.classList.contains('copy-btn')) {
|
if (btn && !btn.classList.contains('copy-btn')) {
|
||||||
btn.classList.add('copy-btn');
|
btn.classList.add('copy-btn');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no button at all, create one
|
||||||
if (!btn) {
|
if (!btn) {
|
||||||
btn = document.createElement('button');
|
btn = document.createElement('button');
|
||||||
btn.type = 'button';
|
btn.type = 'button';
|
||||||
@ -532,6 +555,7 @@ const currentUrl = Astro.url.href;
|
|||||||
pre.appendChild(btn);
|
pre.appendChild(btn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there is a SECOND old button lingering (top-left in your case), hide it
|
||||||
const possibleOldButtons = pre.querySelectorAll(
|
const possibleOldButtons = pre.querySelectorAll(
|
||||||
'.btn-copy, .copy-button, .code-copy, .copy-code, button[aria-label*="copy" i]'
|
'.btn-copy, .copy-button, .code-copy, .copy-code, button[aria-label*="copy" i]'
|
||||||
);
|
);
|
||||||
@ -539,6 +563,7 @@ const currentUrl = Astro.url.href;
|
|||||||
if (b !== btn) b.style.display = 'none';
|
if (b !== btn) b.style.display = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Success pill
|
||||||
if (!pre.querySelector('.copied-pill')) {
|
if (!pre.querySelector('.copied-pill')) {
|
||||||
const pill = document.createElement('div');
|
const pill = document.createElement('div');
|
||||||
pill.className = 'copied-pill';
|
pill.className = 'copied-pill';
|
||||||
@ -546,6 +571,7 @@ const currentUrl = Astro.url.href;
|
|||||||
pre.appendChild(pill);
|
pre.appendChild(pill);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Screen reader live region
|
||||||
if (!pre.querySelector('.sr-live')) {
|
if (!pre.querySelector('.sr-live')) {
|
||||||
const live = document.createElement('div');
|
const live = document.createElement('div');
|
||||||
live.className = 'sr-live';
|
live.className = 'sr-live';
|
||||||
@ -588,13 +614,12 @@ const currentUrl = Astro.url.href;
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make generateTOCContent available globally for the auth check script
|
// keep your existing DOMContentLoaded; just ensure this is called
|
||||||
window.generateTOCContent = generateTOCContent;
|
|
||||||
|
|
||||||
// Initialize everything on page load
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// existing:
|
||||||
calculateReadingTime();
|
calculateReadingTime();
|
||||||
generateSidebarTOC();
|
generateSidebarTOC();
|
||||||
|
// new/updated:
|
||||||
enhanceCodeCopy();
|
enhanceCodeCopy();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -688,245 +688,3 @@
|
|||||||
/* Expand content */
|
/* Expand content */
|
||||||
.article-main { max-width: 100% !important; }
|
.article-main { max-width: 100% !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ==========================================================================
|
|
||||||
VIDEO EMBEDDING - Add to knowledgebase.css
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/* Video Container and Responsive Wrapper */
|
|
||||||
:where(.markdown-content) .video-container {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
margin: 2rem 0;
|
|
||||||
border-radius: var(--radius-lg, 0.75rem);
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: var(--color-bg-tertiary, #000);
|
|
||||||
box-shadow: var(--shadow-lg, 0 12px 30px rgba(0,0,0,0.16));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive 16:9 aspect ratio by default */
|
|
||||||
:where(.markdown-content) .video-container.aspect-16-9 {
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(.markdown-content) .video-container.aspect-4-3 {
|
|
||||||
aspect-ratio: 4 / 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(.markdown-content) .video-container.aspect-1-1 {
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Video Element Styling */
|
|
||||||
:where(.markdown-content) .video-container video {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
background-color: #000;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom Video Controls Enhancement */
|
|
||||||
:where(.markdown-content) video::-webkit-media-controls-panel {
|
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(.markdown-content) video::-webkit-media-controls-current-time-display,
|
|
||||||
:where(.markdown-content) video::-webkit-media-controls-time-remaining-display {
|
|
||||||
color: white;
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Video Loading State */
|
|
||||||
:where(.markdown-content) .video-container .video-loading {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(.markdown-content) .video-container .video-loading .spinner {
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
border: 3px solid var(--color-border);
|
|
||||||
border-top: 3px solid var(--color-primary);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Video Error State */
|
|
||||||
:where(.markdown-content) .video-container .video-error {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
text-align: center;
|
|
||||||
color: var(--color-error, #dc3545);
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(.markdown-content) .video-container .video-error .error-icon {
|
|
||||||
font-size: 3rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Video Metadata Overlay */
|
|
||||||
:where(.markdown-content) .video-metadata {
|
|
||||||
background-color: var(--color-bg-secondary);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-top: none;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
border-radius: 0 0 var(--radius-lg, 0.75rem) var(--radius-lg, 0.75rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(.markdown-content) .video-metadata .video-title {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-text);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(.markdown-content) .video-metadata .video-info {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(.markdown-content) .video-metadata .video-duration,
|
|
||||||
:where(.markdown-content) .video-metadata .video-size,
|
|
||||||
:where(.markdown-content) .video-metadata .video-format {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fullscreen Support */
|
|
||||||
:where(.markdown-content) .video-container video:fullscreen {
|
|
||||||
background-color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(.markdown-content) .video-container video:-webkit-full-screen {
|
|
||||||
background-color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(.markdown-content) .video-container video:-moz-full-screen {
|
|
||||||
background-color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Video Thumbnail/Poster Styling */
|
|
||||||
:where(.markdown-content) .video-container video[poster] {
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Protected Video Overlay */
|
|
||||||
:where(.markdown-content) .video-container .video-protected {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(.markdown-content) .video-container .video-protected .lock-icon {
|
|
||||||
font-size: 3rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
:where(.markdown-content) .video-container {
|
|
||||||
margin: 1.5rem -0.5rem; /* Extend to edges on mobile */
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(.markdown-content) .video-metadata {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(.markdown-content) .video-metadata .video-info {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark Theme Adjustments */
|
|
||||||
[data-theme="dark"] :where(.markdown-content) .video-container {
|
|
||||||
box-shadow: 0 12px 30px rgba(0,0,0,0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] :where(.markdown-content) .video-metadata {
|
|
||||||
background-color: var(--color-bg-tertiary);
|
|
||||||
border-color: color-mix(in srgb, var(--color-border) 60%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Video Caption/Description Support */
|
|
||||||
:where(.markdown-content) .video-caption {
|
|
||||||
margin-top: 1rem;
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
text-align: center;
|
|
||||||
font-style: italic;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Video Gallery Support (multiple videos) */
|
|
||||||
:where(.markdown-content) .video-gallery {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 2rem;
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(.markdown-content) .video-gallery .video-container {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Accessibility Improvements */
|
|
||||||
:where(.markdown-content) .video-container video:focus {
|
|
||||||
outline: 3px solid var(--color-primary);
|
|
||||||
outline-offset: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Print Media - Hide Videos */
|
|
||||||
@media print {
|
|
||||||
:where(.markdown-content) .video-container {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(.markdown-content) .video-container::after {
|
|
||||||
content: "[Video: " attr(data-video-title, "Embedded Video") "]";
|
|
||||||
display: block;
|
|
||||||
padding: 1rem;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
text-align: center;
|
|
||||||
font-style: italic;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1083,6 +1083,7 @@ class ImprovedMicroTaskAIPipeline {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 1: AI selection of tools for completion
|
||||||
const selectionPrompt = AI_PROMPTS.generatePhaseCompletionPrompt(originalQuery, phase, phaseTools, phaseConcepts);
|
const selectionPrompt = AI_PROMPTS.generatePhaseCompletionPrompt(originalQuery, phase, phaseTools, phaseConcepts);
|
||||||
const selectionResult = await this.callMicroTaskAI(selectionPrompt, context, 800);
|
const selectionResult = await this.callMicroTaskAI(selectionPrompt, context, 800);
|
||||||
|
|
||||||
@ -1107,6 +1108,7 @@ class ImprovedMicroTaskAIPipeline {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 2: Generate detailed reasoning for each selected tool
|
||||||
for (const tool of validTools) {
|
for (const tool of validTools) {
|
||||||
console.log('[AI-PIPELINE] Generating reasoning for phase completion tool:', tool.name);
|
console.log('[AI-PIPELINE] Generating reasoning for phase completion tool:', tool.name);
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// src/utils/auth.js
|
// src/utils/auth.js (ENHANCED - Added gated content support)
|
||||||
import type { AstroGlobal } from 'astro';
|
import type { AstroGlobal } from 'astro';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { config } from 'dotenv';
|
import { config } from 'dotenv';
|
||||||
@ -390,10 +390,12 @@ export function getAuthRequirementForContext(context: AuthContextType): boolean
|
|||||||
return getAuthRequirement(context);
|
return getAuthRequirement(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEW: Helper function to check if gated content requires authentication
|
||||||
export function isGatedContentAuthRequired(): boolean {
|
export function isGatedContentAuthRequired(): boolean {
|
||||||
return getAuthRequirement('gatedcontent');
|
return getAuthRequirement('gatedcontent');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEW: Check if specific content should be gated
|
||||||
export function shouldGateContent(isGatedContent: boolean): boolean {
|
export function shouldGateContent(isGatedContent: boolean): boolean {
|
||||||
return isGatedContent && isGatedContentAuthRequired();
|
return isGatedContent && isGatedContentAuthRequired();
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
// src/utils/clientUtils.ts
|
// src/utils/clientUtils.ts
|
||||||
|
// Client-side utilities that mirror server-side toolHelpers.ts
|
||||||
|
|
||||||
export function createToolSlug(toolName: string): string {
|
export function createToolSlug(toolName: string): string {
|
||||||
if (!toolName || typeof toolName !== 'string') {
|
if (!toolName || typeof toolName !== 'string') {
|
||||||
@ -8,10 +8,10 @@ export function createToolSlug(toolName: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return toolName.toLowerCase()
|
return toolName.toLowerCase()
|
||||||
.replace(/[^a-z0-9\s-]/g, '')
|
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
|
||||||
.replace(/\s+/g, '-')
|
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||||
.replace(/-+/g, '-')
|
.replace(/-+/g, '-') // Remove duplicate hyphens
|
||||||
.replace(/^-|-$/g, '');
|
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findToolByIdentifier(tools: any[], identifier: string): any | undefined {
|
export function findToolByIdentifier(tools: any[], identifier: string): any | undefined {
|
||||||
@ -30,6 +30,7 @@ export function isToolHosted(tool: any): boolean {
|
|||||||
tool.projectUrl.trim() !== "";
|
tool.projectUrl.trim() !== "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Consolidated Autocomplete Functionality
|
||||||
interface AutocompleteOptions {
|
interface AutocompleteOptions {
|
||||||
minLength?: number;
|
minLength?: number;
|
||||||
maxResults?: number;
|
maxResults?: number;
|
||||||
@ -96,6 +97,7 @@ export class AutocompleteManager {
|
|||||||
display: none;
|
display: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Insert dropdown after input
|
||||||
const parentElement = this.input.parentNode as HTMLElement;
|
const parentElement = this.input.parentNode as HTMLElement;
|
||||||
parentElement.style.position = 'relative';
|
parentElement.style.position = 'relative';
|
||||||
parentElement.insertBefore(this.dropdown, this.input.nextSibling);
|
parentElement.insertBefore(this.dropdown, this.input.nextSibling);
|
||||||
@ -117,6 +119,7 @@ export class AutocompleteManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.input.addEventListener('blur', () => {
|
this.input.addEventListener('blur', () => {
|
||||||
|
// Delay to allow click events on dropdown items
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const activeElement = document.activeElement;
|
const activeElement = document.activeElement;
|
||||||
if (!activeElement || !this.dropdown.contains(activeElement)) {
|
if (!activeElement || !this.dropdown.contains(activeElement)) {
|
||||||
@ -223,6 +226,7 @@ export class AutocompleteManager {
|
|||||||
})
|
})
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
|
// Bind click events
|
||||||
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
|
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
|
||||||
option.addEventListener('click', () => {
|
option.addEventListener('click', () => {
|
||||||
this.selectItem(this.filteredData[index]);
|
this.selectItem(this.filteredData[index]);
|
||||||
@ -256,6 +260,7 @@ export class AutocompleteManager {
|
|||||||
this.hideDropdown();
|
this.hideDropdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger change event
|
||||||
this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
|
this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
|
||||||
detail: { item, text, selectedItems: Array.from(this.selectedItems) }
|
detail: { item, text, selectedItems: Array.from(this.selectedItems) }
|
||||||
}));
|
}));
|
||||||
@ -302,6 +307,7 @@ export class AutocompleteManager {
|
|||||||
`)
|
`)
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
|
// Bind remove events
|
||||||
this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => {
|
this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => {
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -1,85 +0,0 @@
|
|||||||
// src/utils/remarkVideoPlugin.ts
|
|
||||||
import { visit } from 'unist-util-visit';
|
|
||||||
import type { Plugin } from 'unified';
|
|
||||||
import type { Root } from 'hast';
|
|
||||||
|
|
||||||
|
|
||||||
export const remarkVideoPlugin: Plugin<[], Root> = () => {
|
|
||||||
return (tree: Root) => {
|
|
||||||
visit(tree, 'html', (node: any, index: number | undefined, parent: any) => {
|
|
||||||
if (node.value && node.value.includes('<video') && typeof index === 'number') {
|
|
||||||
|
|
||||||
const srcMatch = node.value.match(/src=["']([^"']+)["']/);
|
|
||||||
const titleMatch = node.value.match(/title=["']([^"']+)["']/);
|
|
||||||
|
|
||||||
if (srcMatch) {
|
|
||||||
const originalSrc = srcMatch[1];
|
|
||||||
const title = titleMatch?.[1] || 'Video';
|
|
||||||
|
|
||||||
const finalSrc = processNextcloudUrl(originalSrc);
|
|
||||||
|
|
||||||
const hasControls = node.value.includes('controls');
|
|
||||||
const hasAutoplay = node.value.includes('autoplay');
|
|
||||||
const hasMuted = node.value.includes('muted');
|
|
||||||
const hasLoop = node.value.includes('loop');
|
|
||||||
const hasPreload = node.value.match(/preload=["']([^"']+)["']/);
|
|
||||||
|
|
||||||
const enhancedHTML = `
|
|
||||||
<div class="video-container aspect-16-9">
|
|
||||||
<video
|
|
||||||
src="${escapeHtml(finalSrc)}"
|
|
||||||
${hasControls ? 'controls' : ''}
|
|
||||||
${hasAutoplay ? 'autoplay' : ''}
|
|
||||||
${hasMuted ? 'muted' : ''}
|
|
||||||
${hasLoop ? 'loop' : ''}
|
|
||||||
${hasPreload ? `preload="${hasPreload[1]}"` : 'preload="metadata"'}
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
data-video-title="${escapeHtml(title)}"
|
|
||||||
data-original-src="${escapeHtml(originalSrc)}"
|
|
||||||
>
|
|
||||||
<p>Your browser does not support the video element.</p>
|
|
||||||
</video>
|
|
||||||
${title !== 'Video' ? `
|
|
||||||
<div class="video-metadata">
|
|
||||||
<div class="video-title">${escapeHtml(title)}</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
parent.children[index] = { type: 'html', value: enhancedHTML };
|
|
||||||
|
|
||||||
console.log(`[VIDEO] Processed: ${title}`);
|
|
||||||
console.log(`[VIDEO] Final URL: ${finalSrc}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
function processNextcloudUrl(originalUrl: string): string {
|
|
||||||
if (isNextcloudShareUrl(originalUrl) && !originalUrl.includes('/download')) {
|
|
||||||
const downloadUrl = `${originalUrl}/download`;
|
|
||||||
console.log(`[VIDEO] Auto-added /download: ${originalUrl} → ${downloadUrl}`);
|
|
||||||
return downloadUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isNextcloudShareUrl(url: string): boolean {
|
|
||||||
const pattern = /\/s\/[a-zA-Z0-9]+/;
|
|
||||||
return pattern.test(url) && (url.includes('nextcloud') || url.includes('cloud.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(unsafe: string): string {
|
|
||||||
if (typeof unsafe !== 'string') return '';
|
|
||||||
|
|
||||||
return unsafe
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/"/g, """)
|
|
||||||
.replace(/'/g, "'");
|
|
||||||
}
|
|
@ -1,5 +1,3 @@
|
|||||||
// src/utils/toolHelpers.ts
|
|
||||||
|
|
||||||
export interface Tool {
|
export interface Tool {
|
||||||
name: string;
|
name: string;
|
||||||
type?: 'software' | 'method' | 'concept';
|
type?: 'software' | 'method' | 'concept';
|
||||||
@ -15,8 +13,31 @@ export interface Tool {
|
|||||||
related_concepts?: string[];
|
related_concepts?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export function createToolSlug(toolName: string): string {
|
||||||
createToolSlug,
|
if (!toolName || typeof toolName !== 'string') {
|
||||||
findToolByIdentifier,
|
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);
|
||||||
isToolHosted
|
return '';
|
||||||
} from './clientUtils.js';
|
}
|
||||||
|
|
||||||
|
return toolName.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
|
||||||
|
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||||
|
.replace(/-+/g, '-') // Remove duplicate hyphens
|
||||||
|
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findToolByIdentifier(tools: Tool[], identifier: string): Tool | undefined {
|
||||||
|
if (!identifier || !Array.isArray(tools)) return undefined;
|
||||||
|
|
||||||
|
return tools.find(tool =>
|
||||||
|
tool.name === identifier ||
|
||||||
|
createToolSlug(tool.name) === identifier.toLowerCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isToolHosted(tool: Tool): boolean {
|
||||||
|
return tool.projectUrl !== undefined &&
|
||||||
|
tool.projectUrl !== null &&
|
||||||
|
tool.projectUrl !== "" &&
|
||||||
|
tool.projectUrl.trim() !== "";
|
||||||
|
}
|
@ -1,115 +0,0 @@
|
|||||||
// src/utils/videoUtils.ts - SIMPLIFIED - Basic utilities only
|
|
||||||
import 'dotenv/config';
|
|
||||||
|
|
||||||
|
|
||||||
export interface SimpleVideoMetadata {
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getVideoMimeType(url: string): string {
|
|
||||||
let extension: string | undefined;
|
|
||||||
try {
|
|
||||||
const pathname = new URL(url).pathname;
|
|
||||||
extension = pathname.split('.').pop()?.toLowerCase();
|
|
||||||
} catch {
|
|
||||||
extension = url.split('?')[0].split('.').pop()?.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
const mimeTypes: Record<string, string> = {
|
|
||||||
mp4: 'video/mp4',
|
|
||||||
webm: 'video/webm',
|
|
||||||
ogg: 'video/ogg',
|
|
||||||
mov: 'video/quicktime',
|
|
||||||
avi: 'video/x-msvideo',
|
|
||||||
m4v: 'video/m4v',
|
|
||||||
mkv: 'video/x-matroska',
|
|
||||||
flv: 'video/x-flv'
|
|
||||||
};
|
|
||||||
|
|
||||||
return (extension && mimeTypes[extension]) || 'video/mp4';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatDuration(seconds: number): string {
|
|
||||||
const hours = Math.floor(seconds / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
const remainingSeconds = Math.floor(seconds % 60);
|
|
||||||
|
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatFileSize(bytes: number): string {
|
|
||||||
if (bytes < 1024) return `${bytes} B`;
|
|
||||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
||||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
||||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function escapeHtml(unsafe: string): string {
|
|
||||||
if (typeof unsafe !== 'string') return '';
|
|
||||||
|
|
||||||
return unsafe
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/"/g, """)
|
|
||||||
.replace(/'/g, "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateVideoHTML(
|
|
||||||
src: string,
|
|
||||||
options: {
|
|
||||||
title?: string;
|
|
||||||
controls?: boolean;
|
|
||||||
autoplay?: boolean;
|
|
||||||
muted?: boolean;
|
|
||||||
loop?: boolean;
|
|
||||||
preload?: 'none' | 'metadata' | 'auto';
|
|
||||||
aspectRatio?: '16:9' | '4:3' | '1:1';
|
|
||||||
showMetadata?: boolean;
|
|
||||||
} = {}
|
|
||||||
): string {
|
|
||||||
const {
|
|
||||||
title = 'Video',
|
|
||||||
controls = true,
|
|
||||||
autoplay = false,
|
|
||||||
muted = false,
|
|
||||||
loop = false,
|
|
||||||
preload = 'metadata',
|
|
||||||
aspectRatio = '16:9',
|
|
||||||
showMetadata = true
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const aspectClass = `aspect-${aspectRatio.replace(':', '-')}`;
|
|
||||||
const videoAttributes = [
|
|
||||||
controls ? 'controls' : '',
|
|
||||||
autoplay ? 'autoplay' : '',
|
|
||||||
muted ? 'muted' : '',
|
|
||||||
loop ? 'loop' : '',
|
|
||||||
`preload="${preload}"`
|
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
|
|
||||||
const metadataHTML = showMetadata && title !== 'Video' ? `
|
|
||||||
<div class="video-metadata">
|
|
||||||
<div class="video-title">${escapeHtml(title)}</div>
|
|
||||||
</div>
|
|
||||||
` : '';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="video-container ${aspectClass}">
|
|
||||||
<video
|
|
||||||
src="${escapeHtml(src)}"
|
|
||||||
${videoAttributes}
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
data-video-title="${escapeHtml(title)}"
|
|
||||||
>
|
|
||||||
<p>Your browser does not support the video element.</p>
|
|
||||||
</video>
|
|
||||||
${metadataHTML}
|
|
||||||
</div>
|
|
||||||
`.trim();
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user