diff --git a/spicecloud/pages/_app.tsx b/spicecloud/pages/_app.tsx index 6c6a9ea..9f4a292 100644 --- a/spicecloud/pages/_app.tsx +++ b/spicecloud/pages/_app.tsx @@ -1,6 +1,17 @@ import '../styles/globals.css' import type { AppProps } from 'next/app' +import Head from 'next/head' +import React from 'react' export default function App({ Component, pageProps }: AppProps) { - return + return ( + <> + + SpiceCloud | Powered by SpiceCodeCLI + + + + + ) + } \ No newline at end of file diff --git a/spicecloud/pages/_document.tsx b/spicecloud/pages/_document.tsx index 628a733..f0a5df0 100644 --- a/spicecloud/pages/_document.tsx +++ b/spicecloud/pages/_document.tsx @@ -3,7 +3,9 @@ import { Html, Head, Main, NextScript } from "next/document"; export default function Document() { return ( - + + +
diff --git a/spicecloud/pages/api/files.ts b/spicecloud/pages/api/files.ts new file mode 100644 index 0000000..90b5be1 --- /dev/null +++ b/spicecloud/pages/api/files.ts @@ -0,0 +1,15 @@ +// pages/api/files.ts +import { promises as fs } from 'fs'; +import path from 'path'; + +export default async function handler(req: any, res: { status: (arg0: number) => { (): any; new(): any; json: { (arg0: { error: string; }): void; new(): any; }; }; }) { + try { + const filePath = path.join(process.cwd(), 'data', 'metrics.json'); + const fileContent = await fs.readFile(filePath, 'utf-8'); + const jsonData = JSON.parse(fileContent); + res.status(200).json(jsonData); + } catch (err) { + console.error('Erro ao ler metrics.json', err); + res.status(500).json({ error: 'Failed to load metrics.json' }); + } +} diff --git a/spicecloud/pages/components/EmptyState.tsx b/spicecloud/pages/components/EmptyState.tsx new file mode 100644 index 0000000..45f197b --- /dev/null +++ b/spicecloud/pages/components/EmptyState.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { styles } from '../utils/styles'; + +export const EmptyState: React.FC = () => { + return ( +
+
+
👆
+

Select a file from the sidebar to view its metrics

+
+
+ ); +}; \ No newline at end of file diff --git a/spicecloud/pages/components/FileHeader.tsx b/spicecloud/pages/components/FileHeader.tsx new file mode 100644 index 0000000..352e17b --- /dev/null +++ b/spicecloud/pages/components/FileHeader.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { MetricData } from '../utils/types'; +import { getFileIconSrc } from '../utils/utils'; +import { styles } from '../utils/styles'; + +interface FileHeaderProps { + selectedFile: MetricData; +} + +export const FileHeader: React.FC = ({ selectedFile }) => { + return ( +
+
+
+ {selectedFile.metrics.file_extension} +
+ +
+

{selectedFile.file_name}

+

+ 📁 {selectedFile.file_path} +

+
+ 🕒 {selectedFile.readable_timestamp} + 💾 {selectedFile.metrics.file_size ? `${(selectedFile.metrics.file_size / 1024).toFixed(1)} KB` : 'N/A'} + 📂 {selectedFile.metrics.file_extension || 'N/A'} +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/spicecloud/pages/components/FileList.tsx b/spicecloud/pages/components/FileList.tsx new file mode 100644 index 0000000..e8c6770 --- /dev/null +++ b/spicecloud/pages/components/FileList.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { MetricData } from '../utils/types'; +import { getFileIconSrc, formatAge } from '../utils/utils'; +import { styles } from '../utils/styles'; + +interface FileListProps { + data: MetricData[]; + selectedFile: MetricData | null; + onFileSelect: (file: MetricData) => void; +} + +export const FileList: React.FC = ({ data, selectedFile, onFileSelect }) => { + return ( +
+

+ 📁 + Analyzed Files +

+
+ {data.map((item) => ( +
onFileSelect(item)} + style={{ + ...styles.fileItem, + ...(selectedFile?.id === item.id + ? styles.fileItemSelected + : styles.fileItemDefault) + }} + className="file-item" + onMouseEnter={(e) => { + if (selectedFile?.id !== item.id) { + e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)'; + } + }} + onMouseLeave={(e) => { + if (selectedFile?.id !== item.id) { + e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)'; + } + }} + > +
+
+ {item.metrics.file_extension} +
+ +
+
+ {item.file_name} +
+
+ {item.file_path.replace(item.file_name, '')} +
+
+ 🕒 + {formatAge(item.age)} +
+
+
+
+ ))} +
+
+ ); +}; \ No newline at end of file diff --git a/spicecloud/pages/components/Header.tsx b/spicecloud/pages/components/Header.tsx new file mode 100644 index 0000000..59d15f9 --- /dev/null +++ b/spicecloud/pages/components/Header.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { styles } from '../utils/styles'; + +interface HeaderProps { + dataLength: number; + loading: boolean; + onRefresh: () => void; +} + +export const Header: React.FC = ({ dataLength, loading, onRefresh }) => { + return ( +
+
+
+

+ SpiceCode Logo + SpiceCloud | Powered by SpiceCodeCLI +

+

{dataLength} files analyzed

+
+ +
+
+ ); +}; \ No newline at end of file diff --git a/spicecloud/pages/components/IndentationCard.tsx b/spicecloud/pages/components/IndentationCard.tsx new file mode 100644 index 0000000..d882dcb --- /dev/null +++ b/spicecloud/pages/components/IndentationCard.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { styles } from '../utils/styles'; + +interface IndentationCardProps { + indentationType?: string; + indentationSize?: number; +} + +export const IndentationCard: React.FC = ({ + indentationType, + indentationSize +}) => { + return ( +
+
+ 📐 +

Indentation

+
+
+
+ Type: + + {indentationType || 'N/A'} + +
+
+ Size: + + {indentationSize || 'N/A'} + +
+
+
+ ); +}; \ No newline at end of file diff --git a/spicecloud/pages/components/LoadingSpinner.tsx b/spicecloud/pages/components/LoadingSpinner.tsx new file mode 100644 index 0000000..2c431ba --- /dev/null +++ b/spicecloud/pages/components/LoadingSpinner.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { styles } from '../utils/styles'; + +export const LoadingSpinner: React.FC = () => { + return ( +
+
+
+

Loading your code metrics...

+
+
+ ); +}; \ No newline at end of file diff --git a/spicecloud/pages/components/MethodTypeCard.tsx b/spicecloud/pages/components/MethodTypeCard.tsx new file mode 100644 index 0000000..59e8c77 --- /dev/null +++ b/spicecloud/pages/components/MethodTypeCard.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { styles } from '../utils/styles'; + +interface MethodTypeCardProps { + value: { + public: number; + private: number; + }; +} + +export const MethodTypeCard: React.FC = ({ value }) => { + return ( +
+
+ 🔧 +

Method Types

+
+
+
+
+ + Public +
+ {value.public} +
+
+
+ + Private +
+ {value.private} +
+
+
+ ); +}; \ No newline at end of file diff --git a/spicecloud/pages/components/MetricCard.tsx b/spicecloud/pages/components/MetricCard.tsx new file mode 100644 index 0000000..85f7bb4 --- /dev/null +++ b/spicecloud/pages/components/MetricCard.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { formatMetricValue, getMetricIcon, getMetricColor, formatLabel } from '../utils/utils'; +import { styles } from '../utils/styles'; + +interface MetricCardProps { + metricKey: string; + value: any; +} + +export const MetricCard: React.FC = ({ metricKey, value }) => { + return ( +
{ + e.currentTarget.style.transform = 'scale(1.05)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = 'scale(1)'; + }} + > +
+ + {getMetricIcon(metricKey)} + +

+ {formatLabel(metricKey)} +

+
+
+ {formatMetricValue(metricKey, value)} +
+ {metricKey.includes('ratio') && ( +
+
+
+ )} +
+ ); +}; \ No newline at end of file diff --git a/spicecloud/pages/components/MetricsGrid.tsx b/spicecloud/pages/components/MetricsGrid.tsx new file mode 100644 index 0000000..1cbeda7 --- /dev/null +++ b/spicecloud/pages/components/MetricsGrid.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { MetricData } from '../utils/types'; +import { MetricCard } from './MetricCard'; +import { MethodTypeCard } from './MethodTypeCard'; +import { IndentationCard } from './IndentationCard'; +import { styles } from '../utils/styles'; + +interface MetricsGridProps { + selectedFile: MetricData; +} + +export const MetricsGrid: React.FC = ({ selectedFile }) => { + return ( +
+ {Object.entries(selectedFile.metrics).map(([key, value]) => { + // Skip file info metrics + if (['file_name', 'file_path', 'file_size', 'file_extension'].includes(key)) { + return null; + } + + // Handle method type count specially + if (key === 'method_type_count' && typeof value === 'object' && value !== null) { + return ( + + ); + } + + // Skip indentation keys as they'll be handled in their own card + if (key === 'indentation_type' || key === 'indentation_size') { + return null; + } + + return ( + + ); + })} + + {/* Indentation Card */} + {(selectedFile.metrics.indentation_type || selectedFile.metrics.indentation_size) && ( + + )} +
+ ); +}; \ No newline at end of file diff --git a/spicecloud/pages/index.tsx b/spicecloud/pages/index.tsx index 5b37717..caeeb4c 100644 --- a/spicecloud/pages/index.tsx +++ b/spicecloud/pages/index.tsx @@ -1,245 +1,57 @@ -import { useState, useEffect } from 'react'; - -interface MetricData { - id: string; - hash: string; - timestamp: number; - file_name: string; - file_path: string; - metrics: any; - age: number; - readable_timestamp: string; -} +import React from 'react'; +import { useData } from './utils/useData'; +import { Header } from './components/Header'; +import { FileList } from './components/FileList'; +import { FileHeader } from './components/FileHeader'; +import { MetricsGrid } from './components/MetricsGrid'; +import { LoadingSpinner } from './components/LoadingSpinner'; +import { EmptyState } from './components/EmptyState'; +import { styles } from './utils/styles'; export default function Home() { - const [data, setData] = useState([]); - const [selectedFile, setSelectedFile] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchData = async () => { - try { - setLoading(true); - const response = await fetch('/api/data'); - const result = await response.json(); - - if (result.success) { - setData(result.data); - if (result.data.length > 0 && !selectedFile) { - setSelectedFile(result.data[0]); - } - } else { - setError('Failed to load data'); - } - } catch (err) { - setError('Error fetching data'); - console.error(err); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchData(); - const interval = setInterval(fetchData, 5000); // Refresh every 5 seconds - return () => clearInterval(interval); - }, []); - - const formatMetricValue = (key: string, value: any): string => { - if (typeof value === 'object' && value !== null) { - return JSON.stringify(value, null, 2); - } - if (key.includes('ratio') && typeof value === 'number') { - return `${(value * 100).toFixed(2)}%`; - } - return String(value); - }; - - const getMetricColor = (key: string): string => { - if (key.includes('count') || key.includes('line')) return 'text-blue-600'; - if (key.includes('ratio')) return 'text-green-600'; - if (key.includes('dependencies')) return 'text-orange-600'; - if (key.includes('indentation')) return 'text-purple-600'; - return 'text-gray-600'; - }; - - const formatAge = (age: number): string => { - const hours = Math.floor(age / (1000 * 60 * 60)); - const minutes = Math.floor((age % (1000 * 60 * 60)) / (1000 * 60)); - - if (hours > 0) { - return `${hours}h ${minutes}m ago`; - } - return `${minutes}m ago`; - }; + const { data, selectedFile, setSelectedFile, loading, error, fetchData } = useData(); if (loading && data.length === 0) { - return ( -
-
-
-

Loading metrics data...

-
-
- ); - } - - if (error) { - return ( -
-
-

⚠️ {error}

- -
-
- ); - } - - if (data.length === 0) { - return ( -
-
-
📊
-

No Analysis Data Yet

-

- Run your CLI analyzer and submit data to see metrics here. -

-
-

# Example usage:

-

python -m cli.main analyze file.py --json

-
- -
-
- ); + return ; } return ( -
-
-
-

Code Quality Metrics Dashboard

-
-

{data.length} files analyzed

- -
-
- -
- {/* File List */} -
-
-

Analyzed Files

-
- {data.map((item) => ( -
setSelectedFile(item)} - className={`p-3 rounded-lg cursor-pointer transition-colors ${ - selectedFile?.id === item.id - ? 'bg-blue-100 border-2 border-blue-300' - : 'bg-gray-50 hover:bg-gray-100 border-2 border-transparent' - }`} - > -
- {item.file_name} -
-
- {item.file_path} -
-
- {formatAge(item.age)} -
-
- ))} -
-
-
- - {/* Metrics Details */} -
- {selectedFile ? ( -
-
-

{selectedFile.file_name}

-

- 📁 {selectedFile.file_path} -

-

- 🕒 Analyzed {selectedFile.readable_timestamp} -

-
- -
- {Object.entries(selectedFile.metrics).map(([key, value]) => { - if (key === 'method_type_count' && typeof value === 'object' && value !== null) { - return ( -
-

Method Types

-
-
- Public: - {(value as any).public} -
-
- Private: - {(value as any).private} -
-
-
- ); - } - - return ( -
-

- {key.replace(/_/g, ' ')} -

-

- {formatMetricValue(key, value)} -

-
- ); - })} - - {/* File Info */} -
-

File Size

-

- {selectedFile.metrics.file_size ? `${(selectedFile.metrics.file_size / 1024).toFixed(2)} KB` : 'N/A'} -

-
- -
-

File Extension

-

- {selectedFile.metrics.file_extension || 'N/A'} -

-
-
-
- ) : ( -
-

Select a file to view its metrics

-
- )} -
+
+ + +
+ +
+ + +
+ {selectedFile ? ( + <> + + + + ) : ( + + )}
diff --git a/spicecloud/pages/utils/styles.ts b/spicecloud/pages/utils/styles.ts new file mode 100644 index 0000000..5c8c2f0 --- /dev/null +++ b/spicecloud/pages/utils/styles.ts @@ -0,0 +1,313 @@ +export const styles = { + container: { + minHeight: '100vh', + background: 'linear-gradient(135deg, #f0e3c8, #e3d6b0, #d7b377)', + fontFamily: 'system-ui, -apple-system, sans-serif', + color: '#000000' + }, + header: { + background: 'rgba(215, 179, 119, 0.9)', + backdropFilter: 'blur(10px)', + borderBottom: '1px solid rgba(160, 130, 60, 0.8)', + position: 'sticky' as const, + top: 0, + zIndex: 40, + padding: '1rem 1.5rem', + color: '#000000', + }, + headerContent: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + maxWidth: '1200px', + margin: '0 auto' + }, + title: { + fontSize: '1.5rem', + fontWeight: 'bold', + color: '#000000', + display: 'flex', + alignItems: 'center', + gap: '0.75rem' + }, + subtitle: { + color: 'rgba(0, 0, 0, 0.7)', + marginTop: '0.25rem' + }, + refreshButton: { + padding: '0.5rem 1rem', + borderRadius: '0.5rem', + fontWeight: '500', + transition: 'all 0.2s', + border: 'none', + cursor: 'pointer', + color: '#000000', + }, + mainContent: { + maxWidth: '1200px', + margin: '0 auto', + padding: '2rem 1.5rem', + display: 'grid', + gridTemplateColumns: 'minmax(300px, 1fr) 2fr', + gap: '2rem' + }, + sidebar: { + background: 'rgba(215, 179, 119, 0.25)', + backdropFilter: 'blur(10px)', + borderRadius: '1rem', + padding: '1.5rem', + border: '1px solid rgba(160, 130, 60, 0.5)', + height: 'fit-content', + position: 'sticky' as const, + top: '6rem', + color: '#000000' + }, + sidebarTitle: { + fontSize: '1.25rem', + fontWeight: '600', + color: '#000000', + marginBottom: '1.5rem', + display: 'flex', + alignItems: 'center', + gap: '0.5rem' + }, + fileList: { + display: 'flex', + flexDirection: 'column' as const, + gap: '0.75rem', + maxHeight: '60vh', + overflowY: 'auto' as const + }, + fileItem: { + padding: '1rem', + borderRadius: '0.75rem', + cursor: 'pointer', + transition: 'all 0.2s', + border: '1px solid rgba(160, 130, 60, 0.3)', + background: 'rgba(215, 179, 119, 0.1)', + color: '#000000', + }, + fileItemSelected: { + background: 'linear-gradient(45deg, #d97304, #a35402)', + color: '#000000', + transform: 'scale(1.02)', + boxShadow: '0 10px 25px rgba(217, 115, 4, 0.5)' + }, + fileItemDefault: { + background: 'rgba(215, 179, 119, 0.1)', + color: '#000000' + }, + fileInfo: { + display: 'flex', + alignItems: 'flex-start', + gap: '0.75rem' + }, + fileIcon: { + fontSize: '1.25rem' + }, + fileName: { + fontWeight: '500', + fontSize: '0.875rem', + marginBottom: '0.25rem' + }, + filePath: { + fontSize: '0.75rem', + opacity: 0.7, + marginBottom: '0.5rem' + }, + fileAge: { + fontSize: '0.75rem', + opacity: 0.7, + display: 'flex', + alignItems: 'center', + gap: '0.25rem' + }, + metricsArea: { + display: 'flex', + flexDirection: 'column' as const, + gap: '1.5rem' + }, + fileHeader: { + background: 'linear-gradient(135deg, rgba(215, 179, 119, 0.3), rgba(160, 130, 60, 0.2))', + backdropFilter: 'blur(10px)', + borderRadius: '1rem', + padding: '1.5rem', + border: '1px solid rgba(160, 130, 60, 0.5)', + color: '#000000' + }, + fileHeaderContent: { + display: 'flex', + alignItems: 'flex-start', + gap: '1rem' + }, + fileHeaderIcon: { + fontSize: '2.5rem' + }, + fileHeaderTitle: { + fontSize: '1.5rem', + fontWeight: 'bold', + color: '#000000', + marginBottom: '0.5rem' + }, + fileHeaderPath: { + color: 'rgba(0, 0, 0, 0.7)', + fontSize: '0.875rem', + marginBottom: '0.5rem' + }, + fileHeaderMeta: { + display: 'flex', + alignItems: 'center', + gap: '1rem', + fontSize: '0.875rem', + color: 'rgba(0, 0, 0, 0.5)' + }, + metricsGrid: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', + gap: '1.5rem' + }, + metricCard: { + borderRadius: '1rem', + padding: '1.5rem', + border: '1px solid rgba(160, 130, 60, 0.5)', + backdropFilter: 'blur(10px)', + background: 'rgba(217, 115, 4, 0.15)', + transition: 'all 0.2s', + cursor: 'default', + color: '#000000' + }, + metricHeader: { + display: 'flex', + alignItems: 'center', + gap: '0.75rem', + marginBottom: '1rem', + color: '#000000' + }, + metricIcon: { + fontSize: '1.5rem', + transition: 'transform 0.2s' + }, + metricTitle: { + fontWeight: '600', + color: '#000000', + fontSize: '1.125rem' + }, + metricValue: { + fontSize: '2rem', + fontWeight: 'bold', + color: '#000000', + marginBottom: '0.5rem' + }, + progressBar: { + width: '100%', + height: '0.5rem', + background: 'rgba(160, 130, 60, 0.3)', + borderRadius: '0.25rem', + marginTop: '0.75rem', + overflow: 'hidden' + }, + progressFill: { + height: '100%', + background: 'linear-gradient(90deg, #d97304, #a35402)', + borderRadius: '0.25rem', + transition: 'width 1s ease' + }, + methodTypeCard: { + background: 'linear-gradient(135deg, rgba(217, 115, 4, 0.2), rgba(161, 84, 2, 0.2))', + border: '1px solid rgba(217, 115, 4, 0.2)', + color: '#000000' + }, + methodTypeList: { + display: 'flex', + flexDirection: 'column' as const, + gap: '0.75rem' + }, + methodTypeItem: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '0.75rem', + background: 'rgba(160, 130, 60, 0.1)', + borderRadius: '0.5rem', + color: '#000000' + }, + methodTypeLabel: { + display: 'flex', + alignItems: 'center', + gap: '0.5rem' + }, + methodTypeDot: { + width: '0.75rem', + height: '0.75rem', + borderRadius: '50%' + }, + methodTypeText: { + color: '#000000' + }, + methodTypeValue: { + fontSize: '1.25rem', + fontWeight: 'bold', + color: '#000000' + }, + indentationCard: { + background: 'linear-gradient(135deg, rgba(217, 115, 4, 0.2), rgba(161, 84, 2, 0.2))', + border: '1px solid rgba(217, 115, 4, 0.2)', + color: '#000000' + }, + indentationList: { + display: 'flex', + flexDirection: 'column' as const, + gap: '0.75rem' + }, + indentationItem: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + color: '#000000' + }, + indentationLabel: { + color: '#000000' + }, + indentationValue: { + fontSize: '1.125rem', + fontWeight: '600', + color: '#000000' + }, + loading: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + minHeight: '100vh', + background: 'linear-gradient(135deg, #f0e3c8, #e3d6b0, #d7b377)', + color: '#000000' + }, + loadingSpinner: { + width: '4rem', + height: '4rem', + border: '4px solid rgba(217, 115, 4, 0.3)', + borderTop: '4px solid #d97304', + borderRadius: '50%', + animation: 'spin 1s linear infinite' + }, + emptyState: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '24rem', + color: '#000000' + }, + emptyStateContent: { + textAlign: 'center' as const + }, + emptyStateIcon: { + fontSize: '3.75rem', + marginBottom: '1rem', + opacity: 0.5 + }, + emptyStateText: { + color: 'rgba(0, 0, 0, 0.7)', + fontSize: '1.25rem' + }, + fileIconImg: { width: '1.5rem', height: '1.5rem', objectFit: 'contain' as const }, + fileHeaderImg: { width: '3rem', height: '3rem', objectFit: 'contain' as const } +}; \ No newline at end of file diff --git a/spicecloud/pages/utils/types.ts b/spicecloud/pages/utils/types.ts new file mode 100644 index 0000000..86a681d --- /dev/null +++ b/spicecloud/pages/utils/types.ts @@ -0,0 +1,10 @@ +export interface MetricData { + id: string; + hash: string; + timestamp: number; + file_name: string; + file_path: string; + metrics: any; + age: number; + readable_timestamp: string; +} \ No newline at end of file diff --git a/spicecloud/pages/utils/useData.ts b/spicecloud/pages/utils/useData.ts new file mode 100644 index 0000000..4065dc2 --- /dev/null +++ b/spicecloud/pages/utils/useData.ts @@ -0,0 +1,57 @@ +import { useState, useEffect } from 'react'; +import { MetricData } from './types'; + +export const useData = () => { + const [data, setData] = useState([]); + const [selectedFile, setSelectedFile] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = async () => { + try { + setLoading(true); + + // Adjust the route if your endpoint is different + const res = await fetch('/api/files'); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + + const json = await res.json(); + + const files = json.map((f: any) => ({ + ...f, + age: Date.now() - f.timestamp, + readable_timestamp: new Date(f.timestamp).toLocaleString('pt-BR'), + // If comment_ratio comes as string ("16.23%") and you prefer number: + metrics: { + ...f.metrics, + comment_ratio: typeof f.metrics.comment_ratio === 'string' + ? parseFloat(f.metrics.comment_ratio) / 100 + : f.metrics.comment_ratio + } + })); + + setData(files); + if (files.length && !selectedFile) setSelectedFile(files[0]); + } catch (err) { + setError('Erro ao buscar dados'); + console.error(err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(); + const interval = setInterval(fetchData, 10000); + return () => clearInterval(interval); + }, []); + + return { + data, + selectedFile, + setSelectedFile, + loading, + error, + fetchData + }; +}; \ No newline at end of file diff --git a/spicecloud/pages/utils/utils.ts b/spicecloud/pages/utils/utils.ts new file mode 100644 index 0000000..3144011 --- /dev/null +++ b/spicecloud/pages/utils/utils.ts @@ -0,0 +1,60 @@ +export const getFileIconSrc = (ext: string): string => { + switch (ext) { + case '.py': return '/python-logo.png'; + case '.js': return '/javascript-logo.png'; + case '.rb': return '/ruby-logo.png'; + case '.go': return '/go-logo.png'; + default: return '/file-icon.png'; + } +}; + +export const formatMetricValue = (key: string, value: any): string => { + if (typeof value === 'object' && value !== null) { + return JSON.stringify(value, null, 2); + } + if (key.includes('ratio') && typeof value === 'number') { + return `${(value * 100).toFixed(1)}%`; + } + return String(value); +}; + +export const getMetricIcon = (key: string): string => { + if (key.includes('line_count')) return '📄'; + if (key.includes('function_count')) return '⚡'; + if (key.includes('comment')) return '💬'; + if (key.includes('dependencies')) return '📦'; + if (key.includes('indentation')) return '📐'; + if (key.includes('method')) return '🔧'; + if (key.includes('ratio')) return '📊'; + return '📋'; +}; + +export const getMetricColor = (key: string): string => { + if (key.includes('count') || key.includes('line')) + return 'linear-gradient(135deg, #e3d6b0, #c9b67a)'; + if (key.includes('ratio')) + return 'linear-gradient(135deg, #d97304, #a35402)'; + if (key.includes('dependencies')) + return 'linear-gradient(135deg, #a67c23, #6b4c1b)'; + if (key.includes('indentation')) + return 'linear-gradient(135deg, #d7b377, #f0e3c8)'; + if (key.includes('method')) + return 'linear-gradient(135deg, #a35402, #d97304)'; + return 'linear-gradient(135deg, #6b4c1b, #4a2c0b)'; +}; + +export const formatAge = (age: number): string => { + const hours = Math.floor(age / (1000 * 60 * 60)); + const minutes = Math.floor((age % (1000 * 60 * 60)) / (1000 * 60)); + + if (hours > 0) { + return `${hours}h ${minutes}m ago`; + } + return `${minutes}m ago`; +}; + +export const formatLabel = (key: string): string => { + return key + .replace(/_/g, ' ') + .replace(/\b\w/g, l => l.toUpperCase()); +}; \ No newline at end of file diff --git a/spicecloud/public/file-icon.png b/spicecloud/public/file-icon.png new file mode 100644 index 0000000..e95bb65 Binary files /dev/null and b/spicecloud/public/file-icon.png differ diff --git a/spicecloud/public/go-logo.png b/spicecloud/public/go-logo.png new file mode 100644 index 0000000..d6d98c3 Binary files /dev/null and b/spicecloud/public/go-logo.png differ diff --git a/spicecloud/public/javascript-logo.png b/spicecloud/public/javascript-logo.png new file mode 100644 index 0000000..7b5ffab Binary files /dev/null and b/spicecloud/public/javascript-logo.png differ diff --git a/spicecloud/public/python-logo.png b/spicecloud/public/python-logo.png new file mode 100644 index 0000000..49ea8f5 Binary files /dev/null and b/spicecloud/public/python-logo.png differ diff --git a/spicecloud/public/ruby-logo.png b/spicecloud/public/ruby-logo.png new file mode 100644 index 0000000..797167a Binary files /dev/null and b/spicecloud/public/ruby-logo.png differ diff --git a/spicecloud/public/spicecode-logo.png b/spicecloud/public/spicecode-logo.png new file mode 100644 index 0000000..71dd73f Binary files /dev/null and b/spicecloud/public/spicecode-logo.png differ diff --git a/spicecloud/tailwind.config.js b/spicecloud/tailwind.config.js index 47bc0ba..8bfaecf 100644 --- a/spicecloud/tailwind.config.js +++ b/spicecloud/tailwind.config.js @@ -1,9 +1,9 @@ -/** @type {import('tailwindcss').Config} */ module.exports = { content: [ - './pages/**/*.{js,ts,jsx,tsx,mdx}', - './components/**/*.{js,ts,jsx,tsx,mdx}', - './app/**/*.{js,ts,jsx,tsx,mdx}', + "./pages/**/*.{js,ts,jsx,tsx}", + "./components/**/*.{js,ts,jsx,tsx}", + "./app/**/*.{js,ts,jsx,tsx}", + // Add your specific paths here ], theme: { extend: {},