@@ -4,6 +4,140 @@ definePageMeta({
44 mainClass: " " ,
55});
66
7+ import { ref , onMounted } from ' vue' ;
8+
9+ // Define types for the repository and log data
10+ interface RepositoryOwner {
11+ id? : string ;
12+ login: string ;
13+ avatar_url: string ;
14+ }
15+
16+ interface LatestCommit {
17+ sha: string ;
18+ message: string ;
19+ date: string ;
20+ }
21+
22+ interface Repository {
23+ id: string ;
24+ name: string ;
25+ owner: RepositoryOwner ;
26+ full_name: string ;
27+ description: string | null ;
28+ default_branch: string ;
29+ html_url: string ;
30+ homepage? : string | null ;
31+ stargazers_count: number ;
32+ watchers_count: number ;
33+ forks_count: number ;
34+ open_issues_count? : number ;
35+ indexed_at: string ;
36+ latest_commit: LatestCommit | null ;
37+ }
38+
39+ interface LogEntry {
40+ timestamp: string ;
41+ message: string ;
42+ type: ' info' | ' error' | ' success' ;
43+ data: any ;
44+ }
45+
46+ // State for repositories and logs
47+ const repositories = ref <Repository []>([]);
48+ const loading = ref (true );
49+ const error = ref <string | null >(null );
50+ const clientLogs = ref <LogEntry []>([]);
51+ const showLogs = ref (false );
52+
53+ // Function to add logs that will be visible in the client
54+ const logToClient = (message : string , type : ' info' | ' error' | ' success' = ' info' , data : any = null ): void => {
55+ const timestamp = new Date ().toISOString ();
56+ const logEntry: LogEntry = {
57+ timestamp ,
58+ message ,
59+ type ,
60+ data
61+ };
62+ clientLogs .value .unshift (logEntry );
63+ console .log (` [CLIENT-LOG][${type }] ${message } ` , data || ' ' );
64+ };
65+
66+ // Fetch all repositories from R2 storage
67+ const fetchRepositories = async (): Promise <void > => {
68+ try {
69+ loading .value = true ;
70+ logToClient (' Fetching repositories from R2 storage...' , ' info' );
71+
72+ const response = await fetch (' /api/repos' );
73+ const responseData = await response .json () as {
74+ repositories: Repository [];
75+ error? : boolean ;
76+ message? : string ;
77+ debug_info? : any ;
78+ };
79+
80+ if (responseData .error ) {
81+ logToClient (` Error fetching repositories: ${responseData .message || ' Unknown error' } ` , ' error' , responseData .debug_info );
82+ error .value = responseData .message || ' Unknown error' ;
83+ loading .value = false ;
84+ return ;
85+ }
86+
87+ repositories .value = responseData .repositories ;
88+ logToClient (
89+ ` Successfully fetched ${responseData .repositories .length } repositories from R2 storage ` ,
90+ ' success' ,
91+ responseData .debug_info
92+ );
93+
94+ // Log available repositories
95+ repositories .value .forEach (repo => {
96+ logToClient (` Repository available: ${repo .full_name } ` , ' info' , {
97+ default_branch: repo .default_branch ,
98+ latest_commit: repo .latest_commit ?
99+ ` ${repo .latest_commit .sha .substring (0 , 7 )} - ${repo .latest_commit .message .split (' \n ' )[0 ]} ` :
100+ ' No commits found'
101+ });
102+ });
103+ } catch (e : any ) {
104+ logToClient (` Failed to fetch repositories: ${e .message } ` , ' error' );
105+ error .value = ` Failed to fetch repositories: ${e .message } ` ;
106+ } finally {
107+ loading .value = false ;
108+ }
109+ };
110+
111+ // Truncate long texts
112+ const truncate = (text : string , length = 60 ): string => {
113+ if (! text ) return ' ' ;
114+ return text .length > length ? text .substring (0 , length ) + ' ...' : text ;
115+ };
116+
117+ // Format date
118+ const formatDate = (dateString : string ): string => {
119+ if (! dateString ) return ' Unknown' ;
120+ const date = new Date (dateString );
121+ return date .toLocaleString ();
122+ };
123+
124+ // Toggle logs visibility
125+ const toggleLogs = (): void => {
126+ showLogs .value = ! showLogs .value ;
127+ };
128+
129+ // Clear logs
130+ const clearLogs = (): void => {
131+ clientLogs .value = [];
132+ logToClient (' Logs cleared' , ' info' );
133+ };
134+
135+ // Load repositories on mount
136+ onMounted (() => {
137+ fetchRepositories ();
138+ });
139+
140+ // Template refs
7141const gettingStartedEl = useTemplateRef (" getting-started" );
8142
9143function scrollToGettingStarted() {
@@ -15,14 +149,131 @@ function scrollToGettingStarted() {
15149
16150<template >
17151 <div >
18- <div
19- class =" my-container flex flex-col items-center gap-4 md:gap-12 min-h-[calc(100vh-80px)]"
20- >
152+ <div class =" my-container flex flex-col items-center gap-4 md:gap-8 min-h-[calc(100vh-80px)]" >
21153 <img src =" /favicon.svg" alt =" logo" width =" 64" height =" 64" />
22154
155+ <!-- Repository search -->
23156 <RepoSearch />
24157
25- <div class =" flex-1" />
158+ <!-- Repository List -->
159+ <div class =" w-full max-w-4xl mt-4" >
160+ <h2 class =" text-xl font-semibold mb-4" >Available Repositories in R2 Storage</h2 >
161+
162+ <div v-if =" loading" class =" p-4 text-center" >
163+ <div class =" animate-spin rounded-full h-10 w-10 border-b-2 border-primary-500 mx-auto" ></div >
164+ <p class =" mt-2" >Loading repositories...</p >
165+ </div >
166+
167+ <div v-else-if =" error" class =" p-4 border border-red-300 bg-red-50 dark:bg-red-900/20 dark:border-red-800 rounded-lg" >
168+ <p class =" text-red-600 dark:text-red-400" >{{ error }}</p >
169+ <button @click =" fetchRepositories" class =" mt-2 px-4 py-2 bg-primary-500 text-white rounded hover:bg-primary-600 transition" >
170+ Try Again
171+ </button >
172+ </div >
173+
174+ <div v-else-if =" repositories.length === 0" class =" p-4 border border-gray-300 dark:border-gray-700 rounded-lg" >
175+ <p >No repositories found in R2 storage.</p >
176+ </div >
177+
178+ <div v-else class =" grid gap-4 md:grid-cols-2" >
179+ <div v-for =" repo in repositories" :key =" repo.id"
180+ class =" border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:shadow-md transition" >
181+ <div class =" flex items-center gap-3 mb-2" >
182+ <img v-if =" repo.owner.avatar_url" :src =" repo.owner.avatar_url" alt =" avatar" class =" w-8 h-8 rounded-full" />
183+ <div v-else class =" w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center" >
184+ <span >{{ repo.owner.login[0] }}</span >
185+ </div >
186+
187+ <h3 class =" font-semibold" >
188+ <NuxtLink :to =" `/view/${repo.owner.login}/${repo.name}`" class =" text-primary-500 hover:underline" >
189+ {{ repo.full_name }}
190+ </NuxtLink >
191+ </h3 >
192+ </div >
193+
194+ <p v-if =" repo.description" class =" text-sm text-gray-600 dark:text-gray-300 mb-3" >
195+ {{ truncate(repo.description) }}
196+ </p >
197+
198+ <div class =" mt-2 text-sm text-gray-500 dark:text-gray-400" >
199+ <div class =" flex gap-2 items-center" >
200+ <UIcon name =" i-heroicons-code-bracket" />
201+ <span >Default branch: {{ repo.default_branch }}</span >
202+ </div >
203+
204+ <div class =" flex gap-2 items-center mt-1" >
205+ <UIcon name =" i-heroicons-star" />
206+ <span >{{ repo.stargazers_count || 0 }} stars</span >
207+ </div >
208+
209+ <div class =" mt-2" >
210+ <p class =" font-medium" >Latest commit:</p >
211+ <div v-if =" repo.latest_commit" class =" bg-gray-50 dark:bg-gray-800 p-2 rounded mt-1" >
212+ <div class =" flex gap-2 items-center" >
213+ <UIcon name =" i-heroicons-hashtag" />
214+ <span class =" text-xs font-mono" >{{ repo.latest_commit.sha.substring(0, 7) }}</span >
215+ </div >
216+ <p class =" text-xs mt-1" >{{ truncate(repo.latest_commit.message, 80) }}</p >
217+ <p class =" text-xs mt-1 text-gray-400" >{{ formatDate(repo.latest_commit.date) }}</p >
218+ </div >
219+ <p v-else class =" text-yellow-600 dark:text-yellow-400 italic" >No commits found</p >
220+ </div >
221+
222+ <div class =" text-xs mt-2 text-gray-400" >
223+ Indexed: {{ formatDate(repo.indexed_at) }}
224+ </div >
225+ </div >
226+ </div >
227+ </div >
228+ </div >
229+
230+ <!-- Client-side Logs Panel -->
231+ <div class =" fixed bottom-4 right-4 z-50" >
232+ <button @click =" toggleLogs"
233+ class =" px-4 py-2 bg-gray-700 text-white rounded-full flex items-center gap-2 hover:bg-gray-800 transition" >
234+ <UIcon name =" i-heroicons-bug-ant" />
235+ <span >{{ showLogs ? 'Hide' : 'Show' }} Logs</span >
236+ <span class =" bg-red-500 rounded-full px-2 text-xs" >{{ clientLogs.length }}</span >
237+ </button >
238+
239+ <div v-if =" showLogs" class =" mt-2 bg-gray-800 text-white p-4 rounded-lg w-96 max-h-96 overflow-y-auto shadow-xl" >
240+ <div class =" flex justify-between items-center mb-2" >
241+ <h3 class =" font-bold" >Client Logs</h3 >
242+ <button @click =" clearLogs" class =" text-xs px-2 py-1 bg-red-600 rounded hover:bg-red-700" >
243+ Clear
244+ </button >
245+ </div >
246+
247+ <div v-if =" clientLogs.length === 0" class =" text-center py-4 text-gray-400" >
248+ No logs yet
249+ </div >
250+
251+ <div v-for =" (log, index) in clientLogs" :key =" index"
252+ :class =" `mb-2 p-2 rounded text-xs ${
253+ log.type === 'error' ? 'bg-red-900/50 border-l-2 border-red-600' :
254+ log.type === 'success' ? 'bg-green-900/50 border-l-2 border-green-600' :
255+ 'bg-gray-700/50 border-l-2 border-gray-500'
256+ }`" >
257+ <div class =" flex justify-between text-xs" >
258+ <span class =" font-mono" >{{ new Date(log.timestamp).toLocaleTimeString() }}</span >
259+ <span :class =" `font-bold ${
260+ log.type === 'error' ? 'text-red-400' :
261+ log.type === 'success' ? 'text-green-400' :
262+ 'text-blue-400'
263+ }`" >
264+ {{ log.type.toUpperCase() }}
265+ </span >
266+ </div >
267+ <div class =" mt-1" >{{ log.message }}</div >
268+ <div v-if =" log.data"
269+ class =" mt-1 p-1 bg-black/30 rounded font-mono text-gray-300 overflow-x-auto" >
270+ <pre >{{ typeof log.data === 'string' ? log.data : JSON.stringify(log.data, null, 2) }}</pre >
271+ </div >
272+ </div >
273+ </div >
274+ </div >
275+
276+ <div class =" flex-1" ></div >
26277
27278 <div
28279 class =" flex flex-col items-center gap-2"
@@ -60,3 +311,18 @@ function scrollToGettingStarted() {
60311 </div >
61312 </div >
62313</template >
314+
315+ <style scoped>
316+ .animate-spin {
317+ animation : spin 1s linear infinite ;
318+ }
319+
320+ @keyframes spin {
321+ from {
322+ transform : rotate (0deg );
323+ }
324+ to {
325+ transform : rotate (360deg );
326+ }
327+ }
328+ </style >
0 commit comments