From 22666529d7a09e5c687bf3e7b12eb32075261707 Mon Sep 17 00:00:00 2001 From: Kayela Claybon <34044644+kayela-c@users.noreply.github.com> Date: Thu, 15 Jan 2026 17:29:27 -0600 Subject: [PATCH] feat: add Gitea support with new widgets and API integration --- .env | 6 +- README.md | 1 + src/App.tsx | 9 +- src/components/GitHubWidget/index.ts | 2 - .../GitHubCommitItem.tsx | 15 +- .../GitHubWidget.tsx | 56 ++- src/components/GitWidget/GiteaCommitItem.tsx | 55 +++ src/components/GitWidget/GiteaWidget.tsx | 200 +++++++++++ src/components/GitWidget/index.ts | 2 + src/styles/{widget.css => GitHubWidget.css} | 40 +-- src/styles/GiteaWidget.css | 324 ++++++++++++++++++ src/styles/index.css | 2 +- src/utils/giteaApi.ts | 77 +++++ src/utils/githubApi.ts | 27 +- 14 files changed, 738 insertions(+), 78 deletions(-) delete mode 100644 src/components/GitHubWidget/index.ts rename src/components/{GitHubWidget => GitWidget}/GitHubCommitItem.tsx (74%) rename src/components/{GitHubWidget => GitWidget}/GitHubWidget.tsx (51%) create mode 100644 src/components/GitWidget/GiteaCommitItem.tsx create mode 100644 src/components/GitWidget/GiteaWidget.tsx create mode 100644 src/components/GitWidget/index.ts rename src/styles/{widget.css => GitHubWidget.css} (79%) create mode 100644 src/styles/GiteaWidget.css create mode 100644 src/utils/giteaApi.ts diff --git a/.env b/.env index e9b5f94..362de50 100644 --- a/.env +++ b/.env @@ -1 +1,5 @@ -REACT_APP_GITHUB_TOKEN=your_github_token_here \ No newline at end of file +REACT_APP_GITHUB_TOKEN=your_github_token_here +REACT_APP_GITEA_TOKEN=your_gitea_token_here +GITEA_API_URL='https://git.konceptkit.com/' +GITHUB_API_URL='https://api.github.com/' +GITEA_ACCESS_TOKEN='de037d3d8b7268acd0dc734a83799e4f3761bad3' \ No newline at end of file diff --git a/README.md b/README.md index c2214f2..4e95718 100644 --- a/README.md +++ b/README.md @@ -78,3 +78,4 @@ npm install recharts # Commit activity charts npm install react-tooltip # Tooltips for commit details +Is there a limit with gitea Api? \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index d091f8a..f38fdfa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,20 @@ // In your main app -import GitHubWidget from "./components/GitHubWidget"; +import { GitHubWidget, GiteaWidget } from "./components/GitWidget"; export default function App() { return (
+
); } diff --git a/src/components/GitHubWidget/index.ts b/src/components/GitHubWidget/index.ts deleted file mode 100644 index 0ace454..0000000 --- a/src/components/GitHubWidget/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// components/GitHubWidget/index.js -export { default } from './GitHubWidget'; \ No newline at end of file diff --git a/src/components/GitHubWidget/GitHubCommitItem.tsx b/src/components/GitWidget/GitHubCommitItem.tsx similarity index 74% rename from src/components/GitHubWidget/GitHubCommitItem.tsx rename to src/components/GitWidget/GitHubCommitItem.tsx index 0db9862..5c8faff 100644 --- a/src/components/GitHubWidget/GitHubCommitItem.tsx +++ b/src/components/GitWidget/GitHubCommitItem.tsx @@ -1,4 +1,3 @@ -// components/GitHubWidget/GitHubCommitItem.jsx import React from 'react'; import { formatDistanceToNow } from 'date-fns'; import { FaCodeBranch, FaUser, FaCalendar } from 'react-icons/fa'; @@ -9,34 +8,34 @@ const GitHubCommitItem = ({ commit }) => { return (
-
+
{sha.substring(0, 7)} - + {formatDistanceToNow(new Date(commitData.author.date))} ago
-
+
{commitData.message.split('\n')[0]}
{author && ( -
+
{commitData.author.name} - + {commitData.author.name}
diff --git a/src/components/GitHubWidget/GitHubWidget.tsx b/src/components/GitWidget/GitHubWidget.tsx similarity index 51% rename from src/components/GitHubWidget/GitHubWidget.tsx rename to src/components/GitWidget/GitHubWidget.tsx index 7e77409..837cc9f 100644 --- a/src/components/GitHubWidget/GitHubWidget.tsx +++ b/src/components/GitWidget/GitHubWidget.tsx @@ -1,20 +1,20 @@ -// components/GitHubWidget/GitHubWidget.jsx -import React, { useState } from 'react'; -import useGitHubCommits from '../../hooks/useGitHubCommits'; -import GitHubCommitItem from './GitHubCommitItem'; -import { FaGithub, FaSync, FaExclamationTriangle } from 'react-icons/fa'; +import React, { useState } from "react"; +import useGitHubCommits from "../../hooks/useGitHubCommits"; +import GitHubCommitItem from "./GitHubCommitItem"; +import { FaGithub, FaSync, FaExclamationTriangle } from "react-icons/fa"; +import "../../styles/GitHubWidget.css"; -const GitHubWidget = ({ - defaultRepo = 'facebook/react', +export const GitHubWidget = ({ + defaultRepo = "facebook/react", token = null, limit = 5, - title = 'Recent Commits' + title = "Recent Commits", }) => { const [repo, setRepo] = useState(defaultRepo); const [inputRepo, setInputRepo] = useState(defaultRepo); - - const { commits, loading, error } = useGitHubCommits(repo, token, limit); + + const { commits, loading, error } = useGitHubCommits(repo, token); const handleRepoChange = (e) => { setInputRepo(e.target.value); @@ -22,17 +22,17 @@ const GitHubWidget = ({ const handleSubmit = (e) => { e.preventDefault(); - if (inputRepo.includes('/')) { + if (inputRepo.includes("/")) { setRepo(inputRepo); } }; return ( -
-
+
+

{title}

-
+ -
-
+
{loading ? ( -
Loading commits...
+
Loading commits...
) : error ? ( -
+

Error: {error}

) : ( -
+
{commits.length > 0 ? ( commits.map((commit) => ( - + )) ) : ( -
No commits found
+
No commits found
)}
)}
- -
); }; -export default GitHubWidget; \ No newline at end of file diff --git a/src/components/GitWidget/GiteaCommitItem.tsx b/src/components/GitWidget/GiteaCommitItem.tsx new file mode 100644 index 0000000..646525b --- /dev/null +++ b/src/components/GitWidget/GiteaCommitItem.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { formatDistanceToNow } from 'date-fns'; +import { FaCodeBranch, FaUser, FaCalendar, FaCode } from 'react-icons/fa'; + +const GiteaCommitItem = ({ commit, giteaUrl, repo }) => { + const { sha, commit: commitData, author, html_url } = commit; + + // Gitea's commit structure is slightly different + const commitMessage = commitData?.message || ''; + const commitAuthor = commitData?.author?.name || ''; + const commitDate = commitData?.author?.date || ''; + + // Build URL for Gitea + const commitUrl = html_url || `${giteaUrl}/${repo}/commit/${sha}`; + + return ( +
+
+ + {sha.substring(0, 7)} + + + {commitDate ? formatDistanceToNow(new Date(commitDate)) + ' ago' : 'Unknown date'} + +
+ +
+ {commitMessage.split('\n')[0]} +
+ +
+ {author?.avatar_url && ( + {commitAuthor} { + e.target.style.display = 'none'; + }} + /> + )} + + {commitAuthor || 'Unknown'} + +
+
+ ); +}; + +export default GiteaCommitItem; \ No newline at end of file diff --git a/src/components/GitWidget/GiteaWidget.tsx b/src/components/GitWidget/GiteaWidget.tsx new file mode 100644 index 0000000..16ef15d --- /dev/null +++ b/src/components/GitWidget/GiteaWidget.tsx @@ -0,0 +1,200 @@ +import React, { useState } from "react"; +import useGiteaCommits from "../../hooks/useGiteaCommits"; +import GiteaCommitItem from "./GiteaCommitItem"; +import "../../styles/GiteaWidget.css"; +import { + FaCodeBranch, + FaSync, + FaExclamationTriangle, + FaServer, +} from "react-icons/fa"; + +export const GiteaWidget = ({ + defaultGiteaUrl = "https://git.konceptkit.com/", + defaultRepo = "owner/repo", + token = null, + limit = 5, + branch = "main", + title = "Recent Commits", +}) => { + const [giteaUrl, setGiteaUrl] = useState(defaultGiteaUrl); + const [repo, setRepo] = useState(defaultRepo); + const [inputGiteaUrl, setInputGiteaUrl] = useState(defaultGiteaUrl); + const [inputRepo, setInputRepo] = useState(defaultRepo); + const [showAdvanced, setShowAdvanced] = useState(false); + + const { commits, loading, error } = useGiteaCommits( + giteaUrl, + repo, + token, + limit, + branch + ); + + const handleSubmit = (e) => { + e.preventDefault(); + + // Validate URL + let url = inputGiteaUrl; + if (!url.startsWith("http")) { + url = `https://${url}`; + } + + // Remove trailing slash if present + url = url.replace(/\/$/, ""); + + setGiteaUrl(url); + + // Validate repo format + if (inputRepo.includes("/")) { + setRepo(inputRepo); + } else { + alert('Repository must be in format "owner/repo"'); + } + }; + + // Example preset repositories + const presetRepos = [ + { label: "Documentation", value: "owner/docs" }, + { label: "API Server", value: "owner/api-server" }, + { label: "Web App", value: "owner/web-app" }, + ]; + + return ( +
+
+ +

{title}

+ + +
+ + {showAdvanced ? ( +
+
+ + setInputGiteaUrl(e.target.value)} + placeholder="https://gitea.example.com" + className="url-input" + /> +
+ +
+ + setInputRepo(e.target.value)} + placeholder="owner/repository" + className="repo-input" + /> +
+ {presetRepos.map((preset) => ( + + ))} +
+
+ + +
+ ) : ( +
+
+ setInputRepo(e.target.value)} + placeholder="owner/repo" + className="simple-input" + /> + +
+
+ )} + +
+ + Instance: {giteaUrl} | Repo: {repo} | + Branch: {branch} + +
+ +
+ {loading ? ( +
+
+ Loading commits from {repo}... +
+ ) : error ? ( +
+ +
+

Failed to load commits

+ {error} +

+ Ensure: +

    +
  • Gitea instance is accessible
  • +
  • Repository exists and is accessible
  • +
  • API token has correct permissions (if private repo)
  • +
+

+
+
+ ) : ( +
+ {commits.length > 0 ? ( + commits.map((commit) => ( + + )) + ) : ( +
+ No commits found in the {branch} branch +
+ )} +
+ )} +
+ +
+ + View on Gitea → + + {commits.length} commits shown +
+
+ ); +}; diff --git a/src/components/GitWidget/index.ts b/src/components/GitWidget/index.ts new file mode 100644 index 0000000..76683e8 --- /dev/null +++ b/src/components/GitWidget/index.ts @@ -0,0 +1,2 @@ +export * from "./GitHubWidget"; +export * from "./GiteaWidget"; diff --git a/src/styles/widget.css b/src/styles/GitHubWidget.css similarity index 79% rename from src/styles/widget.css rename to src/styles/GitHubWidget.css index 982e59e..0b03e6b 100644 --- a/src/styles/widget.css +++ b/src/styles/GitHubWidget.css @@ -6,7 +6,7 @@ margin: 0 auto; } -.widget-header { +.github-widget-header { display: flex; align-items: center; padding: 16px; @@ -19,19 +19,19 @@ font-size: 20px; } -.widget-header h3 { +.github-widget-header h3 { margin: 0; flex-grow: 1; font-size: 14px; font-weight: 600; } -.repo-form { +.github-repo-form { display: flex; gap: 8px; } -.repo-input { +.github-repo-input { padding: 4px 8px; border: 1px solid #d1d5da; border-radius: 3px; @@ -39,7 +39,7 @@ width: 120px; } -.refresh-btn { +.github-refresh-btn { background: #2ea44f; color: white; border: none; @@ -50,7 +50,7 @@ align-items: center; } -.widget-body { +.github-widget-body { padding: 16px; } @@ -63,14 +63,14 @@ border-bottom: none; } -.commit-header { +.github-commit-header { display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 12px; } -.commit-sha { +.github-commit-sha { color: #0366d6; text-decoration: none; display: flex; @@ -78,62 +78,62 @@ gap: 4px; } -.commit-time { +.github-commit-time { color: #6a737d; display: flex; align-items: center; gap: 4px; } -.commit-message { +.github-commit-message { margin-bottom: 8px; font-size: 14px; } -.commit-author { +.github-commit-author { display: flex; align-items: center; gap: 8px; font-size: 12px; } -.author-avatar { +.github-author-avatar { width: 20px; height: 20px; border-radius: 50%; } -.author-name { +.github-author-name { color: #586069; display: flex; align-items: center; gap: 4px; } -.loading, -.error, -.no-commits { +.github-loading, +.github-error, +.github-no-commits { text-align: center; padding: 20px; color: #586069; } -.error { +.github-error { color: #cb2431; } -.widget-footer { +.github-widget-footer { padding: 12px 16px; border-top: 1px solid #e1e4e8; text-align: center; } -.view-all { +.github-view-all { color: #0366d6; text-decoration: none; font-size: 12px; } -.view-all:hover { +.github-view-all:hover { text-decoration: underline; } diff --git a/src/styles/GiteaWidget.css b/src/styles/GiteaWidget.css new file mode 100644 index 0000000..4b0fda6 --- /dev/null +++ b/src/styles/GiteaWidget.css @@ -0,0 +1,324 @@ +/* components/GiteaWidget/GiteaWidget.css */ +.gitea-widget { + background: #f8fafc; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + max-width: 600px; + margin: 0 auto; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.widget-header { + display: flex; + align-items: center; + padding: 16px; + border-bottom: 1px solid #d1d9e0; + background: linear-gradient(135deg, #609926 0%, #467f1c 100%); + color: white; +} + +.gitea-icon { + margin-right: 12px; + font-size: 24px; +} + +.widget-header h3 { + margin: 0; + flex-grow: 1; + font-size: 16px; + font-weight: 600; +} + +.advanced-toggle { + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + color: white; + border-radius: 4px; + padding: 6px 12px; + font-size: 12px; + cursor: pointer; + transition: background 0.2s; +} + +.advanced-toggle:hover { + background: rgba(255, 255, 255, 0.3); +} + +.config-form { + padding: 16px; + background: white; + border-bottom: 1px solid #eaecef; +} + +.form-group { + margin-bottom: 16px; +} + +.form-group label { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + font-size: 14px; + font-weight: 500; + color: #2d3748; +} + +.url-input, +.repo-input { + width: 100%; + padding: 8px 12px; + border: 1px solid #cbd5e0; + border-radius: 4px; + font-size: 14px; +} + +.url-input:focus, +.repo-input:focus { + outline: none; + border-color: #609926; + box-shadow: 0 0 0 3px rgba(96, 153, 38, 0.1); +} + +.preset-repos { + display: flex; + gap: 8px; + margin-top: 8px; + flex-wrap: wrap; +} + +.preset-btn { + background: #edf2f7; + border: 1px solid #cbd5e0; + border-radius: 4px; + padding: 4px 8px; + font-size: 12px; + cursor: pointer; + transition: all 0.2s; +} + +.preset-btn:hover { + background: #e2e8f0; + border-color: #a0aec0; +} + +.simple-form { + padding: 12px 16px; + background: white; + border-bottom: 1px solid #eaecef; +} + +.input-group { + display: flex; + gap: 8px; +} + +.simple-input { + flex-grow: 1; + padding: 8px 12px; + border: 1px solid #cbd5e0; + border-radius: 4px; + font-size: 14px; +} + +.simple-btn { + background: #609926; + color: white; + border: none; + border-radius: 4px; + padding: 8px 16px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.current-config { + padding: 8px 16px; + background: #f1f5f9; + border-bottom: 1px solid #e2e8f0; + font-size: 12px; + color: #64748b; +} + +.current-config code { + background: #e2e8f0; + padding: 2px 4px; + border-radius: 2px; + margin: 0 2px; +} + +.widget-body { + padding: 0; +} + +.gitea-commit-item { + padding: 16px; + border-bottom: 1px solid #eaecef; + background: white; + transition: background 0.2s; +} + +.gitea-commit-item:hover { + background: #f8fafc; +} + +.gitea-commit-item:last-child { + border-bottom: none; +} + +.commit-header { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + font-size: 12px; +} + +.commit-sha { + color: #609926; + text-decoration: none; + display: flex; + align-items: center; + gap: 6px; + font-weight: 500; +} + +.commit-time { + color: #718096; + display: flex; + align-items: center; + gap: 6px; +} + +.commit-message { + margin-bottom: 12px; + font-size: 14px; + line-height: 1.5; + color: #2d3748; +} + +.commit-author { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; +} + +.author-avatar { + width: 20px; + height: 20px; + border-radius: 50%; + object-fit: cover; +} + +.author-name { + color: #4a5568; + display: flex; + align-items: center; + gap: 6px; +} + +.loading { + padding: 40px 20px; + text-align: center; + color: #718096; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} + +.spinner { + width: 40px; + height: 40px; + border: 3px solid #e2e8f0; + border-top: 3px solid #609926; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.error { + padding: 24px; + text-align: center; + color: #e53e3e; +} + +.error-details { + margin-top: 12px; +} + +.error-details small { + color: #718096; + display: block; + margin: 8px 0; +} + +.help-text { + text-align: left; + margin-top: 16px; + color: #4a5568; + font-size: 12px; +} + +.help-text ul { + margin: 8px 0; + padding-left: 20px; +} + +.no-commits { + padding: 40px 20px; + text-align: center; + color: #a0aec0; + font-style: italic; +} + +.widget-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-top: 1px solid #e2e8f0; + background: white; +} + +.view-all { + color: #609926; + text-decoration: none; + font-size: 14px; + font-weight: 500; +} + +.view-all:hover { + text-decoration: underline; +} + +.commit-count { + color: #718096; + font-size: 12px; +} + +.refresh-btn { + background: #609926; + color: white; + border: none; + border-radius: 4px; + padding: 10px 20px; + font-size: 14px; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + transition: background 0.2s; +} + +.refresh-btn:hover { + background: #4d7c1f; +} diff --git a/src/styles/index.css b/src/styles/index.css index fd03978..2816721 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -3,7 +3,7 @@ @import "tailwindcss/components"; @import "tailwindcss/utilities"; */ -@import "./widget.css"; + :root { font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; diff --git a/src/utils/giteaApi.ts b/src/utils/giteaApi.ts new file mode 100644 index 0000000..1d946c2 --- /dev/null +++ b/src/utils/giteaApi.ts @@ -0,0 +1,77 @@ +import axios from 'axios'; + +// Gitea API configuration + +const createGiteaApi = (baseUrl, token = null) => { + const headers = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers.Authorization = `token ${token}`; + } + + const api = axios.create({ + baseURL: `${baseUrl}/api/v1`, + headers, + }); + + return { + // Fetch commits for a repository + fetchCommits: async (owner, repo, limit = 10, branch = 'main') => { + try { + const response = await api.get( + `/repos/${owner}/${repo}/commits`, + { + params: { + limit, + page: 1, + sha: branch, // Optional branch parameter + } + } + ); + return response.data; + } catch (error) { + console.error('Error fetching Gitea commits:', error); + throw error; + } + }, + + // Fetch user repositories + fetchUserRepos: async (username) => { + try { + const response = await api.get(`/users/${username}/repos`); + return response.data; + } catch (error) { + console.error('Error fetching user repos:', error); + throw error; + } + }, + + // Fetch repository information + fetchRepoInfo: async (owner, repo) => { + try { + const response = await api.get(`/repos/${owner}/${repo}`); + return response.data; + } catch (error) { + console.error('Error fetching repo info:', error); + throw error; + } + }, + + // Search repositories + searchRepos: async (query) => { + try { + const response = await api.get('/repos/search', { + params: { q: query, limit: 10 } + }); + return response.data.data; + } catch (error) { + console.error('Error searching repos:', error); + throw error; + } + } + }; +}; + +export default createGiteaApi; \ No newline at end of file diff --git a/src/utils/githubApi.ts b/src/utils/githubApi.ts index 5dbbf1d..a5fa21f 100644 --- a/src/utils/githubApi.ts +++ b/src/utils/githubApi.ts @@ -1,11 +1,11 @@ // utils/githubApi.js -import axios from 'axios'; +import axios from "axios"; -const GITHUB_API = 'https://api.github.com'; +const GITHUB_API = "https://api.github.com"; export const fetchCommits = async (repo, token = null, limit = 10) => { - const [owner, repoName] = repo.split('/'); - + const [owner, repoName] = repo.split("/"); + const headers = {}; if (token) { headers.Authorization = `token ${token}`; @@ -18,14 +18,14 @@ export const fetchCommits = async (repo, token = null, limit = 10) => { headers, params: { per_page: limit, - page: 1 - } + page: 1, + }, } ); - + return response.data; } catch (error) { - console.error('Error fetching GitHub commits:', error); + console.error("Error fetching GitHub commits:", error); throw error; } }; @@ -37,13 +37,12 @@ export const fetchUserRepos = async (username, token = null) => { } try { - const response = await axios.get( - `${GITHUB_API}/users/${username}/repos`, - { headers } - ); + const response = await axios.get(`${GITHUB_API}/users/${username}/repos`, { + headers, + }); return response.data; } catch (error) { - console.error('Error fetching user repos:', error); + console.error("Error fetching user repos:", error); throw error; } -}; \ No newline at end of file +};