mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 10:06:42 +02:00
chore: add techbase to apps-archived
Integrated techbase (software comparison platform) into monorepo structure: - Created NestJS backend with votes and comments modules - Migrated from external Supabase to own PostgreSQL - Set up Drizzle ORM schema for votes and comments - Created API client replacing Supabase in Astro frontend - Added environment configuration (port 3021) Archived immediately as it's not yet ready for active development. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
17313473aa
commit
34c879929b
161 changed files with 12613 additions and 0 deletions
115
apps-archived/techbase/apps/web/src/utils/api.ts
Normal file
115
apps-archived/techbase/apps/web/src/utils/api.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/**
|
||||
* API Client for TechBase Backend
|
||||
* Replaces Supabase with direct calls to NestJS backend
|
||||
*/
|
||||
|
||||
const API_URL = import.meta.env.PUBLIC_BACKEND_URL || 'http://localhost:3021';
|
||||
|
||||
interface VoteResult {
|
||||
success: boolean;
|
||||
newAverage: number;
|
||||
voteCount: number;
|
||||
}
|
||||
|
||||
interface CommentResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface MetricsResult {
|
||||
metrics: Record<string, { average: number; count: number }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a vote for a software metric
|
||||
*/
|
||||
export async function submitVote(
|
||||
softwareId: string,
|
||||
metric: string,
|
||||
rating: number
|
||||
): Promise<VoteResult> {
|
||||
const res = await fetch(`${API_URL}/api/votes`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ softwareId, metric, rating }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json().catch(() => ({ message: 'Vote failed' }));
|
||||
throw new Error(error.message || 'Failed to submit vote');
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metrics for a specific software
|
||||
*/
|
||||
export async function getMetrics(softwareId: string): Promise<MetricsResult> {
|
||||
const res = await fetch(`${API_URL}/api/votes/${softwareId}/metrics`);
|
||||
|
||||
if (!res.ok) {
|
||||
// Return default metrics on error
|
||||
return {
|
||||
metrics: {
|
||||
easeOfUse: { average: 0, count: 0 },
|
||||
featureRichness: { average: 0, count: 0 },
|
||||
valueForMoney: { average: 0, count: 0 },
|
||||
support: { average: 0, count: 0 },
|
||||
reliability: { average: 0, count: 0 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all metrics for multiple software items
|
||||
*/
|
||||
export async function getAllMetrics(): Promise<
|
||||
Record<string, Record<string, { average: number; count: number }>>
|
||||
> {
|
||||
const res = await fetch(`${API_URL}/api/votes/metrics/all`);
|
||||
|
||||
if (!res.ok) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a comment for review
|
||||
*/
|
||||
export async function submitComment(
|
||||
softwareId: string,
|
||||
userName: string,
|
||||
comment: string
|
||||
): Promise<CommentResult> {
|
||||
const res = await fetch(`${API_URL}/api/comments`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ softwareId, userName, comment }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json().catch(() => ({ message: 'Comment failed' }));
|
||||
throw new Error(error.message || 'Failed to submit comment');
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get approved comments for a software
|
||||
*/
|
||||
export async function getComments(softwareId: string): Promise<any[]> {
|
||||
const res = await fetch(`${API_URL}/api/comments/${softwareId}`);
|
||||
|
||||
if (!res.ok) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
18
apps-archived/techbase/apps/web/src/utils/i18n.ts
Normal file
18
apps-archived/techbase/apps/web/src/utils/i18n.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { i18nConfig } from '../content/config';
|
||||
|
||||
export function getLocalizedUrl(path: string, locale: string) {
|
||||
return `/${locale}${path.startsWith('/') ? path : '/' + path}`;
|
||||
}
|
||||
|
||||
export function getLocaleFromUrl(url: URL) {
|
||||
const [, locale] = url.pathname.split('/');
|
||||
if (i18nConfig.locales.includes(locale)) {
|
||||
return locale;
|
||||
}
|
||||
return i18nConfig.defaultLocale;
|
||||
}
|
||||
|
||||
export async function loadTranslations(locale: string) {
|
||||
const translations = await import(`../content/translations/${locale}.json`);
|
||||
return translations.default;
|
||||
}
|
||||
117
apps-archived/techbase/apps/web/src/utils/search.ts
Normal file
117
apps-archived/techbase/apps/web/src/utils/search.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
// Search utility functions
|
||||
|
||||
/**
|
||||
* Filters software based on category, platform, pricing, and rating filters
|
||||
* @param softwareList The list of software to filter
|
||||
* @param filters The filters to apply
|
||||
* @returns Filtered software list
|
||||
*/
|
||||
export function filterSoftware(softwareList, filters) {
|
||||
return softwareList.filter(software => {
|
||||
// Filter by categories
|
||||
if (filters.categories.length > 0) {
|
||||
if (!software.categories?.some(category => filters.categories.includes(category))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by platforms
|
||||
if (filters.platforms.length > 0) {
|
||||
if (!software.platforms?.some(platform => filters.platforms.includes(platform))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by price range
|
||||
if (filters.priceRanges.length > 0) {
|
||||
const hasFree = filters.priceRanges.includes('free') &&
|
||||
software.pricing?.some(p => p.model.toLowerCase().includes('free') || p.price.includes('0') || p.price.includes('kostenlos'));
|
||||
|
||||
const hasPaid = filters.priceRanges.includes('paid') &&
|
||||
software.pricing?.some(p => !p.model.toLowerCase().includes('free') &&
|
||||
!p.model.toLowerCase().includes('subscription') &&
|
||||
!p.price.includes('0') &&
|
||||
!p.price.includes('kostenlos'));
|
||||
|
||||
const hasSubscription = filters.priceRanges.includes('subscription') &&
|
||||
software.pricing?.some(p => p.model.toLowerCase().includes('subscription') ||
|
||||
p.model.toLowerCase().includes('abonnement') ||
|
||||
p.price.toLowerCase().includes('/month') ||
|
||||
p.price.toLowerCase().includes('/monat'));
|
||||
|
||||
if (!(hasFree || hasPaid || hasSubscription)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by rating thresholds
|
||||
const metrics = software.metrics || {};
|
||||
for (const [metric, threshold] of Object.entries(filters.ratingThresholds)) {
|
||||
if (threshold > 0) {
|
||||
const rating = metrics[metric]?.average || 0;
|
||||
if (rating < threshold) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts software by various criteria
|
||||
* @param softwareList The list of software to sort
|
||||
* @param sortBy The sort criterion
|
||||
* @param sortOrder The sort order ('asc' or 'desc')
|
||||
* @returns Sorted software list
|
||||
*/
|
||||
export function sortSoftware(softwareList, sortBy = 'rating', sortOrder = 'desc') {
|
||||
return [...softwareList].sort((a, b) => {
|
||||
let valueA, valueB;
|
||||
|
||||
switch (sortBy) {
|
||||
case 'name':
|
||||
valueA = a.name.toLowerCase();
|
||||
valueB = b.name.toLowerCase();
|
||||
break;
|
||||
|
||||
case 'rating':
|
||||
// Calculate average rating
|
||||
const metricsA = a.metrics || {};
|
||||
const metricsB = b.metrics || {};
|
||||
|
||||
const ratingsA = Object.values(metricsA).map(m => m.average || 0);
|
||||
const ratingsB = Object.values(metricsB).map(m => m.average || 0);
|
||||
|
||||
valueA = ratingsA.length > 0 ? ratingsA.reduce((sum, r) => sum + r, 0) / ratingsA.length : 0;
|
||||
valueB = ratingsB.length > 0 ? ratingsB.reduce((sum, r) => sum + r, 0) / ratingsB.length : 0;
|
||||
break;
|
||||
|
||||
case 'votes':
|
||||
// Calculate total votes
|
||||
const votesA = Object.values(a.metrics || {}).reduce((sum, m) => sum + (m.count || 0), 0);
|
||||
const votesB = Object.values(b.metrics || {}).reduce((sum, m) => sum + (m.count || 0), 0);
|
||||
|
||||
valueA = votesA;
|
||||
valueB = votesB;
|
||||
break;
|
||||
|
||||
case 'date':
|
||||
valueA = new Date(a.lastUpdated).getTime();
|
||||
valueB = new Date(b.lastUpdated).getTime();
|
||||
break;
|
||||
|
||||
default:
|
||||
valueA = a.name.toLowerCase();
|
||||
valueB = b.name.toLowerCase();
|
||||
}
|
||||
|
||||
// Apply sort order
|
||||
if (sortOrder === 'asc') {
|
||||
return valueA > valueB ? 1 : valueA < valueB ? -1 : 0;
|
||||
} else {
|
||||
return valueA < valueB ? 1 : valueA > valueB ? -1 : 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
66
apps-archived/techbase/apps/web/src/utils/software.js
Normal file
66
apps-archived/techbase/apps/web/src/utils/software.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// Zentraler Daten-Store für Software-Informationen
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.store('software', {
|
||||
// Aktuelle Software, die im Detail angezeigt wird (aus MD-Datei geladen)
|
||||
current: null,
|
||||
|
||||
// Software zum Vergleich (über API geladen)
|
||||
comparison: null,
|
||||
|
||||
// Ähnliche Software-Optionen
|
||||
similar: [],
|
||||
|
||||
// Status-Flags
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
// Modus-Flags
|
||||
compareMode: false,
|
||||
|
||||
// Lädt Vergleichssoftware über API
|
||||
async loadComparisonSoftware(id, locale) {
|
||||
if (!id) return;
|
||||
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/software/${id}.json?lang=${locale}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error loading software: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.comparison = data;
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error loading comparison software:', error);
|
||||
this.error = 'Failed to load software data. Please try again.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Setzt den aktuellen Vergleichsmodus
|
||||
setCompareMode(mode) {
|
||||
this.compareMode = mode;
|
||||
|
||||
if (!mode) {
|
||||
// Bei Beenden des Vergleichsmodus Daten zurücksetzen
|
||||
this.comparison = null;
|
||||
}
|
||||
},
|
||||
|
||||
// Initialisiert ähnliche Software-Optionen
|
||||
initSimilarSoftware(similarOptions) {
|
||||
this.similar = similarOptions || [];
|
||||
},
|
||||
|
||||
// Initialisiert die aktuelle Software
|
||||
initCurrentSoftware(software) {
|
||||
this.current = software;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import { getCollection } from 'astro:content';
|
||||
import { getMetrics } from './api';
|
||||
|
||||
interface SoftwareData {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
logo?: string;
|
||||
screenshots?: string[];
|
||||
features: string[];
|
||||
platforms: string[];
|
||||
categories: string[];
|
||||
pricing: {
|
||||
model: string;
|
||||
price: string;
|
||||
yearly_price?: string;
|
||||
features: string[];
|
||||
}[];
|
||||
website: string;
|
||||
lastUpdated: string;
|
||||
metrics?: Record<string, { average: number; count: number }>;
|
||||
similarSoftware?: any[];
|
||||
}
|
||||
|
||||
export async function loadSoftwareData(entry: any, locale: string): Promise<SoftwareData> {
|
||||
// Extract the software ID
|
||||
const id = entry.id.split('/')[1]; // The format is 'locale/id'
|
||||
|
||||
// Get software data from the entry
|
||||
const software: SoftwareData = {
|
||||
id,
|
||||
...entry.data,
|
||||
};
|
||||
|
||||
// Load metrics from backend
|
||||
await loadMetricsFromBackend(software.id, software);
|
||||
|
||||
// Load similar software
|
||||
await loadSimilarSoftware(software, locale);
|
||||
|
||||
return software;
|
||||
}
|
||||
|
||||
async function loadMetricsFromBackend(softwareId: string, software: SoftwareData): Promise<void> {
|
||||
// Default metrics
|
||||
software.metrics = {
|
||||
easeOfUse: { average: 4.2, count: 15 },
|
||||
featureRichness: { average: 3.8, count: 12 },
|
||||
valueForMoney: { average: 4.5, count: 18 },
|
||||
support: { average: 3.5, count: 10 },
|
||||
reliability: { average: 4.0, count: 14 },
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await getMetrics(softwareId);
|
||||
if (result && result.metrics) {
|
||||
software.metrics = result.metrics;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching metrics from backend:', error);
|
||||
// Continue with default metrics
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSimilarSoftware(software: SoftwareData, locale: string): Promise<void> {
|
||||
try {
|
||||
// Get all software entries
|
||||
const allSoftware = await getCollection('software');
|
||||
|
||||
// Filter similar software based on categories
|
||||
const similarSoftwareEntries = allSoftware.filter(item => {
|
||||
// Skip the current software
|
||||
if (item.id === `${locale}/${software.id}`) return false;
|
||||
|
||||
// Extract the software's locale and categories
|
||||
const [itemLocale, itemId] = item.id.split('/');
|
||||
const itemCategories = item.data.categories || [];
|
||||
|
||||
// Only include software with the same locale and at least one shared category
|
||||
return itemLocale === locale &&
|
||||
itemCategories.some(cat => software.categories.includes(cat));
|
||||
});
|
||||
|
||||
// Convert entries to a more usable format for the frontend
|
||||
software.similarSoftware = similarSoftwareEntries.map(item => {
|
||||
const [itemLocale, itemId] = item.id.split('/');
|
||||
return {
|
||||
id: itemId,
|
||||
name: item.data.name,
|
||||
description: item.data.description,
|
||||
categories: item.data.categories || [],
|
||||
logo: item.data.logo || '/logos/sample-logo.svg'
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading similar software:', error);
|
||||
software.similarSoftware = [];
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue