diff --git a/apps/frontend/src/layouts/Layout.astro b/apps/frontend/src/layouts/Layout.astro index b886dfe..4f25853 100644 --- a/apps/frontend/src/layouts/Layout.astro +++ b/apps/frontend/src/layouts/Layout.astro @@ -14,6 +14,8 @@
diff --git a/apps/frontend/src/pages/code.astro b/apps/frontend/src/pages/code.astro new file mode 100644 index 0000000..7dc2aaf --- /dev/null +++ b/apps/frontend/src/pages/code.astro @@ -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); +--- + + +
+

Recent Commits

+

Latest commits from public repositories on our Gitea instance.

+
+ +
+ {error && ( + + )} + + {!error && recentCommits.length === 0 && ( +

No recent commits found or accessible from public repositories.

+ )} + +
+ {recentCommits.map((commit) => ( +
+
+ 🚀 +
+
+

+ + {commit.repo.owner} / {commit.repo.name} + + : {commit.message.split('\n')[0]} +

+

+ Authored by {commit.author.name} on {new Date(commit.date).toLocaleDateString('en-US', { + year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' + })} + + {commit.sha.substring(0, 7)} + +

+
+
+ ))} +
+ + {!error && publicRepos.length > 0 && ( +
+

Public Repositories

+
+ {publicRepos.map((repo) => ( +
+

+ + {repo.owner.login}/{repo.name} + +

+ {repo.description &&

{repo.description}

} + {repo.language &&

Language: {repo.language}

} +

Updated: {new Date(repo.updated_at).toLocaleDateString('en-US', {year: 'numeric', month: 'short', day: 'numeric'})}

+
+ ))} +
+
+ )} +
+
\ No newline at end of file diff --git a/apps/frontend/src/pages/news.astro b/apps/frontend/src/pages/news.astro new file mode 100644 index 0000000..d18524a --- /dev/null +++ b/apps/frontend/src/pages/news.astro @@ -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 = { + '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' }); +} +--- + + +
+
+ +
+
+
+
+ +
+
+ Latest Updates +
+

+ Wiki News +

+

+ The latest documentation, technical guides, and development logs from our + + Knowledge Base + . +

+
+
+ +
+ + {error && ( + + )} + + {!error && recentPages.length === 0 && ( +
+
📭
+

No pages found

+

It seems like there aren't any public pages available on the wiki at the moment.

+
+ )} + + {!error && recentPages.length > 0 && ( +
+ {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 ( + +
+ + /{page.locale} + +
+ {isNew && ( + + + New + + )} + {timeAgo(page.updatedAt)} +
+
+ +

+ {page.title} +

+ + {page.description ? ( +

+ {page.description} +

+ ) : ( +
No description provided.
+ )} + +
+ + {page.path} + + + Read + +
+
+ ); + })} +
+ )} + + {!error && recentPages.length > 0 && ( + + )} +
+
+
diff --git a/docker-compose.yml b/docker-compose.yml index 07856bc..2d80944 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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