code and news menu items
This commit is contained in:
@@ -14,6 +14,8 @@
|
||||
<nav>
|
||||
<a href="/" class="p-2">Home</a>
|
||||
<a href="/catalog" class="p-2">Catalog</a>
|
||||
<a href="/news" class="p-2">News</a>
|
||||
<a href="/code" class="p-2">Code</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
|
||||
const GITEA_API_BASE_URL = 'https://git.teletype.hu/api/v1';
|
||||
const GITEA_TOKEN = import.meta.env.WEBAPP_GITEA_TOKEN;
|
||||
|
||||
interface GiteaRepo {
|
||||
owner: {
|
||||
login: string;
|
||||
};
|
||||
name: string;
|
||||
description: string;
|
||||
language?: string;
|
||||
html_url: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface Commit {
|
||||
sha: string;
|
||||
message: string;
|
||||
author: { name: string; email: string; };
|
||||
date: string;
|
||||
url: string;
|
||||
repo: {
|
||||
owner: string;
|
||||
name: string;
|
||||
html_url: string;
|
||||
};
|
||||
}
|
||||
|
||||
let allCommits: Commit[] = [];
|
||||
let error: string | null = null;
|
||||
let publicRepos: GiteaRepo[] = [];
|
||||
|
||||
if (!GITEA_TOKEN) {
|
||||
error = "Gitea API token (WEBAPP_GITEA_TOKEN) is not configured. Please set it in your .env file and docker-compose.yml.";
|
||||
} else {
|
||||
try {
|
||||
// Step 1: Fetch public repositories
|
||||
const reposResponse = await fetch(`${GITEA_API_BASE_URL}/repos/search?q=&private=false&limit=50`, {
|
||||
headers: {
|
||||
'Authorization': `token ${GITEA_TOKEN}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!reposResponse.ok) {
|
||||
const errorText = await reposResponse.text();
|
||||
throw new Error(`Gitea API (repos/search) responded with status ${reposResponse.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const reposData = await reposResponse.json();
|
||||
publicRepos = reposData.data || [];
|
||||
|
||||
// Step 2: Fetch recent commits directly for each repository
|
||||
const commitPromises = publicRepos.map(async (repo) => {
|
||||
try {
|
||||
const commitsResponse = await fetch(
|
||||
`${GITEA_API_BASE_URL}/repos/${repo.owner.login}/${repo.name}/commits?limit=10&page=1`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `token ${GITEA_TOKEN}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!commitsResponse.ok) {
|
||||
console.warn(`Failed to fetch commits for ${repo.owner.login}/${repo.name}: ${commitsResponse.status}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const commitsData = await commitsResponse.json();
|
||||
|
||||
if (!Array.isArray(commitsData)) {
|
||||
console.warn(`Unexpected response for ${repo.owner.login}/${repo.name}:`, commitsData);
|
||||
return [];
|
||||
}
|
||||
|
||||
const commits: Commit[] = commitsData.map((c: any) => ({
|
||||
sha: c.sha,
|
||||
message: c.commit?.message ?? '',
|
||||
author: {
|
||||
name: c.commit?.author?.name ?? 'Unknown',
|
||||
email: c.commit?.author?.email ?? '',
|
||||
},
|
||||
date: c.commit?.author?.date ?? c.created,
|
||||
url: c.html_url,
|
||||
repo: {
|
||||
owner: repo.owner.login,
|
||||
name: repo.name,
|
||||
html_url: repo.html_url,
|
||||
}
|
||||
}));
|
||||
|
||||
return commits;
|
||||
} catch (repoCommitError: any) {
|
||||
console.warn(`Error fetching commits for ${repo.owner.login}/${repo.name}: ${repoCommitError.message}`);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const allCommitsArrays = await Promise.all(commitPromises);
|
||||
allCommits = allCommitsArrays.flat();
|
||||
|
||||
// Step 3: Sort all commits by date descending
|
||||
allCommits.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
|
||||
} catch (e: any) {
|
||||
console.error('Failed to fetch Gitea data:', e);
|
||||
error = `Failed to fetch Gitea data: ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Limit to top 20 commits for display
|
||||
const recentCommits = allCommits.slice(0, 20);
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<header class="text-center py-16 bg-gradient-to-r from-teal-600 to-green-600 text-white">
|
||||
<h1 class="text-5xl font-bold mb-4">Recent Commits</h1>
|
||||
<p class="text-xl max-w-xl mx-auto">Latest commits from public repositories on our Gitea instance.</p>
|
||||
</header>
|
||||
|
||||
<main class="max-w-6xl mx-auto py-12 px-4">
|
||||
{error && (
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-8" role="alert">
|
||||
<strong class="font-bold">Error!</strong>
|
||||
<span class="block sm:inline"> {error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && recentCommits.length === 0 && (
|
||||
<p class="text-center text-gray-600 text-lg">No recent commits found or accessible from public repositories.</p>
|
||||
)}
|
||||
|
||||
<div class="space-y-4 mb-12">
|
||||
{recentCommits.map((commit) => (
|
||||
<div class="bg-white shadow-lg rounded-lg p-4 flex items-center space-x-4">
|
||||
<div class="flex-shrink-0 text-3xl">
|
||||
🚀
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<p class="text-gray-800 font-semibold">
|
||||
<a href={commit.repo.html_url} target="_blank" class="text-blue-600 hover:text-blue-800">
|
||||
<span class="font-bold">{commit.repo.owner}</span> / <span class="font-bold">{commit.repo.name}</span>
|
||||
</a>
|
||||
: <span class="text-gray-700">{commit.message.split('\n')[0]}</span>
|
||||
</p>
|
||||
<p class="text-gray-600 text-sm mt-1">
|
||||
Authored by <span class="font-bold">{commit.author.name}</span> on {new Date(commit.date).toLocaleDateString('en-US', {
|
||||
year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
|
||||
})}
|
||||
<a href={commit.url} target="_blank" class="ml-2 text-blue-500 hover:text-blue-700">
|
||||
<span class="font-mono text-xs">{commit.sha.substring(0, 7)}</span>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!error && publicRepos.length > 0 && (
|
||||
<section class="max-w-6xl mx-auto py-8 px-4">
|
||||
<h2 class="text-3xl font-bold mb-6 text-center">Public Repositories</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{publicRepos.map((repo) => (
|
||||
<div class="bg-white shadow-lg rounded-lg p-4">
|
||||
<h3 class="text-xl font-semibold mb-2">
|
||||
<a href={repo.html_url} target="_blank" class="text-blue-600 hover:text-blue-800">
|
||||
{repo.owner.login}/{repo.name}
|
||||
</a>
|
||||
</h3>
|
||||
{repo.description && <p class="text-gray-700 text-sm">{repo.description}</p>}
|
||||
{repo.language && <p class="text-gray-500 text-xs mt-1">Language: {repo.language}</p>}
|
||||
<p class="text-gray-500 text-xs mt-1">Updated: {new Date(repo.updated_at).toLocaleDateString('en-US', {year: 'numeric', month: 'short', day: 'numeric'})}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
</Layout>
|
||||
@@ -0,0 +1,210 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
|
||||
const WIKIJS_BASE_URL = 'https://wiki.teletype.hu';
|
||||
const WIKIJS_TOKEN = import.meta.env.WEBAPP_WIKIJS_TOKEN;
|
||||
|
||||
interface WikiPage {
|
||||
id: number;
|
||||
path: string;
|
||||
title: string;
|
||||
description: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
let recentPages: WikiPage[] = [];
|
||||
let error: string | null = null;
|
||||
|
||||
const GRAPHQL_QUERY = `
|
||||
{
|
||||
pages {
|
||||
list(orderBy: UPDATED, orderByDirection: DESC) {
|
||||
id
|
||||
path
|
||||
title
|
||||
description
|
||||
updatedAt
|
||||
createdAt
|
||||
locale
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
if (WIKIJS_TOKEN) {
|
||||
headers['Authorization'] = `Bearer ${WIKIJS_TOKEN}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${WIKIJS_BASE_URL}/graphql`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ query: GRAPHQL_QUERY }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`WikiJS GraphQL responded with status ${response.status}: ${text}`);
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
if (json.errors) {
|
||||
throw new Error(`GraphQL error: ${json.errors.map((e: any) => e.message).join(', ')}`);
|
||||
}
|
||||
|
||||
const pages: any[] = json?.data?.pages?.list ?? [];
|
||||
|
||||
recentPages = pages.map((p: any) => ({
|
||||
id: p.id,
|
||||
path: p.path,
|
||||
title: p.title || p.path,
|
||||
description: p.description ?? '',
|
||||
updatedAt: p.updatedAt,
|
||||
createdAt: p.createdAt,
|
||||
locale: p.locale,
|
||||
})).slice(0, 30);
|
||||
|
||||
} catch (e: any) {
|
||||
console.error('Failed to fetch WikiJS data:', e);
|
||||
error = `Failed to fetch wiki data: ${e.message}`;
|
||||
}
|
||||
|
||||
function timeAgo(dateStr: string): string {
|
||||
const now = new Date();
|
||||
const date = new Date(dateStr);
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffMins < 1) return 'just now';
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays < 30) return `${diffDays}d ago`;
|
||||
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
}
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<div class="bg-gray-50 min-h-screen pb-20">
|
||||
<header class="relative overflow-hidden bg-slate-900 py-24 text-white">
|
||||
<!-- Decorative background elements -->
|
||||
<div class="absolute inset-0 opacity-30">
|
||||
<div class="absolute -top-24 -left-24 h-96 w-96 rounded-full bg-indigo-600 blur-3xl"></div>
|
||||
<div class="absolute -bottom-24 -right-24 h-96 w-96 rounded-full bg-purple-600 blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative max-w-5xl mx-auto px-4 text-center">
|
||||
<div class="inline-block px-3 py-1 rounded-full bg-indigo-500/20 border border-indigo-400/30 text-indigo-300 text-sm font-medium mb-6 backdrop-blur-sm">
|
||||
Latest Updates
|
||||
</div>
|
||||
<h1 class="text-5xl md:text-6xl font-extrabold tracking-tight mb-6">
|
||||
Wiki <span class="text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 to-purple-400">News</span>
|
||||
</h1>
|
||||
<p class="text-lg md:text-xl text-slate-300 max-w-2xl mx-auto leading-relaxed">
|
||||
The latest documentation, technical guides, and development logs from our
|
||||
<a href={WIKIJS_BASE_URL} target="_blank" class="text-indigo-400 hover:text-indigo-300 underline underline-offset-4 transition-colors font-medium">
|
||||
Knowledge Base
|
||||
</a>.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="max-w-7xl mx-auto px-4 -mt-12 relative z-10">
|
||||
|
||||
{error && (
|
||||
<div class="bg-red-50 border-l-4 border-red-500 p-6 rounded-r-xl shadow-lg mb-12 flex items-start gap-4" role="alert">
|
||||
<span class="text-2xl">⚠️</span>
|
||||
<div>
|
||||
<h3 class="text-red-800 font-bold text-lg">Failed to connect to Wiki</h3>
|
||||
<p class="text-red-700 mt-1">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && recentPages.length === 0 && (
|
||||
<div class="bg-white rounded-2xl shadow-xl p-12 text-center border border-gray-100">
|
||||
<div class="text-6xl mb-4">📭</div>
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-2">No pages found</h2>
|
||||
<p class="text-gray-500 max-w-md mx-auto">It seems like there aren't any public pages available on the wiki at the moment.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && recentPages.length > 0 && (
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{recentPages.map((page) => {
|
||||
const pageUrl = `${WIKIJS_BASE_URL}/${page.locale}/${page.path}`;
|
||||
const isNew = (new Date().getTime() - new Date(page.createdAt).getTime()) < 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={pageUrl}
|
||||
target="_blank"
|
||||
class="group flex flex-col bg-white rounded-2xl border border-gray-200 p-6 shadow-sm hover:shadow-xl hover:border-indigo-300 transition-all duration-300 ease-out hover:-translate-y-1"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="text-sm font-mono text-indigo-500 bg-indigo-50 px-2 py-1 rounded">
|
||||
/{page.locale}
|
||||
</span>
|
||||
<div class="flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
{isNew && (
|
||||
<span class="flex items-center gap-1 text-green-600 bg-green-50 px-2 py-0.5 rounded-full border border-green-100">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse"></span>
|
||||
New
|
||||
</span>
|
||||
)}
|
||||
<span>{timeAgo(page.updatedAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-bold text-gray-900 group-hover:text-indigo-600 transition-colors mb-2 line-clamp-2 leading-tight">
|
||||
{page.title}
|
||||
</h2>
|
||||
|
||||
{page.description ? (
|
||||
<p class="text-gray-600 text-sm mb-4 line-clamp-3 leading-relaxed flex-grow">
|
||||
{page.description}
|
||||
</p>
|
||||
) : (
|
||||
<div class="flex-grow mb-4 italic text-gray-400 text-sm">No description provided.</div>
|
||||
)}
|
||||
|
||||
<div class="pt-4 border-t border-gray-100 mt-auto flex items-center justify-between">
|
||||
<span class="text-xs font-mono text-gray-400 truncate max-w-[180px]">
|
||||
{page.path}
|
||||
</span>
|
||||
<span class="text-indigo-600 text-sm font-bold flex items-center gap-1 group-hover:translate-x-1 transition-transform">
|
||||
Read <span class="text-lg">→</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && recentPages.length > 0 && (
|
||||
<div class="mt-16 text-center">
|
||||
<a
|
||||
href={WIKIJS_BASE_URL}
|
||||
target="_blank"
|
||||
class="inline-flex items-center gap-2 bg-slate-900 text-white px-8 py-4 rounded-xl hover:bg-slate-800 transition-all font-bold shadow-lg hover:shadow-indigo-200/50"
|
||||
>
|
||||
Browse Wiki Home
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</Layout>
|
||||
+2
-2
@@ -110,8 +110,8 @@ services:
|
||||
container_name: frontend
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- HOST=0.0.0.0
|
||||
|
||||
- HOST=0.0.0.0
|
||||
- WEBAPP_GITEA_TOKEN=${WEBAPP_GITEA_TOKEN}
|
||||
volumes:
|
||||
- ./apps/frontend:/app
|
||||
- /app/node_modules
|
||||
|
||||
Reference in New Issue
Block a user