git-history-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +21 -0
- package/README.md +304 -0
- package/demo.js +45 -0
- package/dist/__tests__/gitService.test.d.ts +2 -0
- package/dist/__tests__/gitService.test.d.ts.map +1 -0
- package/dist/__tests__/gitService.test.js +32 -0
- package/dist/__tests__/gitService.test.js.map +1 -0
- package/dist/backend/dev-server.d.ts +2 -0
- package/dist/backend/dev-server.d.ts.map +1 -0
- package/dist/backend/dev-server.js +16 -0
- package/dist/backend/dev-server.js.map +1 -0
- package/dist/backend/gitService.d.ts +45 -0
- package/dist/backend/gitService.d.ts.map +1 -0
- package/dist/backend/gitService.js +239 -0
- package/dist/backend/gitService.js.map +1 -0
- package/dist/backend/server.d.ts +9 -0
- package/dist/backend/server.d.ts.map +1 -0
- package/dist/backend/server.js +118 -0
- package/dist/backend/server.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +49 -0
- package/dist/cli.js.map +1 -0
- package/frontend/.editorconfig +17 -0
- package/frontend/.vscode/extensions.json +4 -0
- package/frontend/.vscode/launch.json +20 -0
- package/frontend/.vscode/tasks.json +42 -0
- package/frontend/README.md +59 -0
- package/frontend/angular.json +99 -0
- package/frontend/package-lock.json +10566 -0
- package/frontend/package.json +55 -0
- package/frontend/proxy.conf.json +7 -0
- package/frontend/public/favicon.ico +0 -0
- package/frontend/src/app/app.component.ts +598 -0
- package/frontend/src/app/app.config.ts +12 -0
- package/frontend/src/app/app.css +0 -0
- package/frontend/src/app/app.html +342 -0
- package/frontend/src/app/app.routes.ts +3 -0
- package/frontend/src/app/app.spec.ts +23 -0
- package/frontend/src/app/app.ts +12 -0
- package/frontend/src/app/components/color-palette-selector/color-palette-selector.component.ts +137 -0
- package/frontend/src/app/components/commit-detail/commit-detail.component.ts +327 -0
- package/frontend/src/app/components/commit-graph/commit-graph.component.ts +294 -0
- package/frontend/src/app/components/commit-list/commit-list.component.ts +199 -0
- package/frontend/src/app/components/diff-viewer/diff-viewer.component.ts +311 -0
- package/frontend/src/app/models/color-palette.models.ts +229 -0
- package/frontend/src/app/models/git.models.ts +39 -0
- package/frontend/src/app/services/git.service.ts +43 -0
- package/frontend/src/index.html +13 -0
- package/frontend/src/main.ts +6 -0
- package/frontend/src/styles.css +397 -0
- package/frontend/tsconfig.app.json +15 -0
- package/frontend/tsconfig.json +34 -0
- package/frontend/tsconfig.spec.json +14 -0
- package/jest.config.js +13 -0
- package/package.json +70 -0
- package/public/app.js +403 -0
- package/public/index.html +172 -0
- package/src/__tests__/gitService.test.ts +35 -0
- package/src/backend/dev-server.ts +14 -0
- package/src/backend/gitService.ts +277 -0
- package/src/backend/server.ts +132 -0
- package/src/cli.ts +56 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "./tsconfig.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"outDir": "./out-tsc/app",
|
|
7
|
+
"types": []
|
|
8
|
+
},
|
|
9
|
+
"include": [
|
|
10
|
+
"src/**/*.ts"
|
|
11
|
+
],
|
|
12
|
+
"exclude": [
|
|
13
|
+
"src/**/*.spec.ts"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"compileOnSave": false,
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noImplicitOverride": true,
|
|
8
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
9
|
+
"noImplicitReturns": true,
|
|
10
|
+
"noFallthroughCasesInSwitch": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"experimentalDecorators": true,
|
|
14
|
+
"importHelpers": true,
|
|
15
|
+
"target": "ES2022",
|
|
16
|
+
"module": "preserve"
|
|
17
|
+
},
|
|
18
|
+
"angularCompilerOptions": {
|
|
19
|
+
"enableI18nLegacyMessageIdFormat": false,
|
|
20
|
+
"strictInjectionParameters": true,
|
|
21
|
+
"strictInputAccessModifiers": true,
|
|
22
|
+
"typeCheckHostBindings": true,
|
|
23
|
+
"strictTemplates": true
|
|
24
|
+
},
|
|
25
|
+
"files": [],
|
|
26
|
+
"references": [
|
|
27
|
+
{
|
|
28
|
+
"path": "./tsconfig.app.json"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"path": "./tsconfig.spec.json"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "./tsconfig.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"outDir": "./out-tsc/spec",
|
|
7
|
+
"types": [
|
|
8
|
+
"jasmine"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"include": [
|
|
12
|
+
"src/**/*.ts"
|
|
13
|
+
]
|
|
14
|
+
}
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
preset: 'ts-jest',
|
|
3
|
+
testEnvironment: 'node',
|
|
4
|
+
roots: ['<rootDir>/src'],
|
|
5
|
+
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
|
|
6
|
+
transform: {
|
|
7
|
+
'^.+\\.ts$': 'ts-jest',
|
|
8
|
+
},
|
|
9
|
+
collectCoverageFrom: [
|
|
10
|
+
'src/**/*.ts',
|
|
11
|
+
'!src/**/*.d.ts',
|
|
12
|
+
],
|
|
13
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "git-history-ui",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Beautiful git history visualization in your browser",
|
|
5
|
+
"main": "dist/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"git-history-ui": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "npm run build:backend && npm run build:frontend",
|
|
11
|
+
"build:backend": "tsc -p tsconfig.json",
|
|
12
|
+
"build:frontend": "cd frontend && ng build --configuration production",
|
|
13
|
+
"dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"",
|
|
14
|
+
"dev:backend": "ts-node-dev --respawn --transpile-only src/backend/dev-server.ts",
|
|
15
|
+
"dev:frontend": "cd frontend && ng serve --port 4200",
|
|
16
|
+
"start": "node dist/backend/server.js",
|
|
17
|
+
"test": "jest",
|
|
18
|
+
"lint": "eslint src/**/*.ts",
|
|
19
|
+
"prepare": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"git",
|
|
23
|
+
"history",
|
|
24
|
+
"visualization",
|
|
25
|
+
"cli",
|
|
26
|
+
"ui",
|
|
27
|
+
"commit",
|
|
28
|
+
"graph",
|
|
29
|
+
"diff",
|
|
30
|
+
"angular"
|
|
31
|
+
],
|
|
32
|
+
"author": "beingmartinbmc",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"chalk": "^5.3.0",
|
|
36
|
+
"commander": "^11.1.0",
|
|
37
|
+
"cors": "^2.8.5",
|
|
38
|
+
"express": "^4.18.2",
|
|
39
|
+
"open": "^9.1.0",
|
|
40
|
+
"simple-git": "^3.20.0",
|
|
41
|
+
"socket.io": "^4.7.4"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/commander": "^2.12.2",
|
|
45
|
+
"@types/cors": "^2.8.17",
|
|
46
|
+
"@types/express": "^4.17.21",
|
|
47
|
+
"@types/jest": "^29.5.0",
|
|
48
|
+
"@types/node": "^20.10.0",
|
|
49
|
+
"@types/open": "^6.2.1",
|
|
50
|
+
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
|
51
|
+
"@typescript-eslint/parser": "^6.13.0",
|
|
52
|
+
"concurrently": "^8.2.2",
|
|
53
|
+
"eslint": "^8.55.0",
|
|
54
|
+
"jest": "^29.7.0",
|
|
55
|
+
"ts-jest": "^29.1.0",
|
|
56
|
+
"ts-node-dev": "^2.0.0",
|
|
57
|
+
"typescript": "^5.3.0"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=18.0.0"
|
|
61
|
+
},
|
|
62
|
+
"repository": {
|
|
63
|
+
"type": "git",
|
|
64
|
+
"url": "https://github.com/beingmartinbmc/git-history-ui.git"
|
|
65
|
+
},
|
|
66
|
+
"bugs": {
|
|
67
|
+
"url": "https://github.com/beingmartinbmc/git-history-ui/issues"
|
|
68
|
+
},
|
|
69
|
+
"homepage": "https://github.com/beingmartinbmc/git-history-ui#readme"
|
|
70
|
+
}
|
package/public/app.js
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
class GitHistoryUI {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.commits = [];
|
|
4
|
+
this.currentView = 'list';
|
|
5
|
+
this.darkMode = false;
|
|
6
|
+
this.socket = io();
|
|
7
|
+
|
|
8
|
+
this.initializeEventListeners();
|
|
9
|
+
this.loadCommits();
|
|
10
|
+
this.setupSocket();
|
|
11
|
+
this.initializeDarkMode();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
initializeEventListeners() {
|
|
15
|
+
// View toggles
|
|
16
|
+
document.getElementById('graphView').addEventListener('click', () => this.switchView('graph'));
|
|
17
|
+
document.getElementById('listView').addEventListener('click', () => this.switchView('list'));
|
|
18
|
+
|
|
19
|
+
// Dark mode toggle
|
|
20
|
+
document.getElementById('darkMode').addEventListener('click', () => this.toggleDarkMode());
|
|
21
|
+
|
|
22
|
+
// Search and filters
|
|
23
|
+
document.getElementById('search').addEventListener('input', (e) => this.handleSearch(e.target.value));
|
|
24
|
+
document.getElementById('authorFilter').addEventListener('change', (e) => this.handleAuthorFilter(e.target.value));
|
|
25
|
+
document.getElementById('sinceFilter').addEventListener('change', (e) => this.handleSinceFilter(e.target.value));
|
|
26
|
+
document.getElementById('fileFilter').addEventListener('input', (e) => this.handleFileFilter(e.target.value));
|
|
27
|
+
|
|
28
|
+
// Modal
|
|
29
|
+
document.getElementById('closeModal').addEventListener('click', () => this.closeModal());
|
|
30
|
+
|
|
31
|
+
// Close modal on outside click
|
|
32
|
+
document.getElementById('commitModal').addEventListener('click', (e) => {
|
|
33
|
+
if (e.target.id === 'commitModal') {
|
|
34
|
+
this.closeModal();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setupSocket() {
|
|
40
|
+
this.socket.on('connect', () => {
|
|
41
|
+
console.log('Connected to server');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
this.socket.on('disconnect', () => {
|
|
45
|
+
console.log('Disconnected from server');
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async loadCommits() {
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch('/api/commits');
|
|
52
|
+
this.commits = await response.json();
|
|
53
|
+
this.renderCommits();
|
|
54
|
+
this.populateFilters();
|
|
55
|
+
this.hideLoading();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('Error loading commits:', error);
|
|
58
|
+
this.showError('Failed to load commits');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async loadCommitsWithFilters(filters = {}) {
|
|
63
|
+
try {
|
|
64
|
+
const params = new URLSearchParams(filters);
|
|
65
|
+
const response = await fetch(`/api/commits?${params}`);
|
|
66
|
+
this.commits = await response.json();
|
|
67
|
+
this.renderCommits();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('Error loading commits with filters:', error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
renderCommits() {
|
|
74
|
+
if (this.currentView === 'graph') {
|
|
75
|
+
this.renderGraphView();
|
|
76
|
+
} else {
|
|
77
|
+
this.renderListView();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
renderListView() {
|
|
82
|
+
const container = document.getElementById('commitsList');
|
|
83
|
+
container.innerHTML = '';
|
|
84
|
+
|
|
85
|
+
this.commits.forEach(commit => {
|
|
86
|
+
const commitElement = this.createCommitElement(commit);
|
|
87
|
+
container.appendChild(commitElement);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
createCommitElement(commit) {
|
|
92
|
+
const div = document.createElement('div');
|
|
93
|
+
div.className = 'bg-white dark:bg-gray-800 rounded-lg shadow p-4 hover:shadow-md transition-all duration-200 cursor-pointer';
|
|
94
|
+
div.innerHTML = `
|
|
95
|
+
<div class="flex items-start justify-between">
|
|
96
|
+
<div class="flex-1">
|
|
97
|
+
<div class="flex items-center space-x-2 mb-2">
|
|
98
|
+
<span class="text-sm font-mono text-gray-500 dark:text-gray-400 transition-colors duration-200">${commit.hash.substring(0, 8)}</span>
|
|
99
|
+
<span class="text-sm text-gray-600 dark:text-gray-400 transition-colors duration-200">${commit.author}</span>
|
|
100
|
+
<span class="text-sm text-gray-500 dark:text-gray-500 transition-colors duration-200">${this.formatDate(commit.date)}</span>
|
|
101
|
+
</div>
|
|
102
|
+
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2 transition-colors duration-200">${commit.message}</h3>
|
|
103
|
+
<div class="flex items-center space-x-4 text-sm text-gray-600 dark:text-gray-400 transition-colors duration-200">
|
|
104
|
+
<span>${commit.files.length} files changed</span>
|
|
105
|
+
${commit.branches.length > 0 ? `<span>Branch: ${commit.branches[0]}</span>` : ''}
|
|
106
|
+
${commit.tags.length > 0 ? `<span>Tag: ${commit.tags[0]}</span>` : ''}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="flex space-x-2">
|
|
110
|
+
<button class="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200" onclick="app.showCommitDetails('${commit.hash}')">
|
|
111
|
+
View
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
`;
|
|
116
|
+
return div;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
renderGraphView() {
|
|
120
|
+
const svg = d3.select('#commitGraph');
|
|
121
|
+
svg.selectAll('*').remove();
|
|
122
|
+
|
|
123
|
+
const width = svg.node().getBoundingClientRect().width;
|
|
124
|
+
const height = 600;
|
|
125
|
+
const margin = { top: 20, right: 20, bottom: 20, left: 20 };
|
|
126
|
+
|
|
127
|
+
// Get colors based on dark mode
|
|
128
|
+
const colors = this.getGraphColors();
|
|
129
|
+
|
|
130
|
+
const g = svg.append('g')
|
|
131
|
+
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
132
|
+
|
|
133
|
+
// Create commit nodes
|
|
134
|
+
const nodes = this.commits.map((commit, i) => ({
|
|
135
|
+
id: commit.hash,
|
|
136
|
+
x: (i % 10) * 80 + 40,
|
|
137
|
+
y: Math.floor(i / 10) * 100 + 50,
|
|
138
|
+
commit: commit
|
|
139
|
+
}));
|
|
140
|
+
|
|
141
|
+
// Create links between commits
|
|
142
|
+
const links = [];
|
|
143
|
+
for (let i = 1; i < nodes.length; i++) {
|
|
144
|
+
links.push({
|
|
145
|
+
source: nodes[i - 1],
|
|
146
|
+
target: nodes[i]
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Draw links
|
|
151
|
+
g.selectAll('.link')
|
|
152
|
+
.data(links)
|
|
153
|
+
.enter().append('line')
|
|
154
|
+
.attr('class', 'link')
|
|
155
|
+
.attr('x1', d => d.source.x)
|
|
156
|
+
.attr('y1', d => d.source.y)
|
|
157
|
+
.attr('x2', d => d.target.x)
|
|
158
|
+
.attr('y2', d => d.target.y)
|
|
159
|
+
.attr('stroke', colors.link)
|
|
160
|
+
.attr('stroke-width', 2);
|
|
161
|
+
|
|
162
|
+
// Draw nodes
|
|
163
|
+
const node = g.selectAll('.commit-node')
|
|
164
|
+
.data(nodes)
|
|
165
|
+
.enter().append('g')
|
|
166
|
+
.attr('class', 'commit-node')
|
|
167
|
+
.attr('transform', d => `translate(${d.x},${d.y})`);
|
|
168
|
+
|
|
169
|
+
node.append('circle')
|
|
170
|
+
.attr('r', 8)
|
|
171
|
+
.attr('fill', colors.nodeFill)
|
|
172
|
+
.attr('stroke', colors.nodeStroke)
|
|
173
|
+
.attr('stroke-width', 2);
|
|
174
|
+
|
|
175
|
+
node.append('text')
|
|
176
|
+
.attr('text-anchor', 'middle')
|
|
177
|
+
.attr('dy', 25)
|
|
178
|
+
.attr('fill', colors.text)
|
|
179
|
+
.attr('class', 'text-xs')
|
|
180
|
+
.text(d => d.commit.hash.substring(0, 6));
|
|
181
|
+
|
|
182
|
+
// Add click handlers
|
|
183
|
+
node.on('click', (event, d) => {
|
|
184
|
+
this.showCommitDetails(d.commit.hash);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async showCommitDetails(hash) {
|
|
189
|
+
try {
|
|
190
|
+
const [commit, diff] = await Promise.all([
|
|
191
|
+
fetch(`/api/commit/${hash}`).then(r => r.json()),
|
|
192
|
+
fetch(`/api/diff/${hash}`).then(r => r.json())
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
const modal = document.getElementById('commitModal');
|
|
196
|
+
const details = document.getElementById('commitDetails');
|
|
197
|
+
|
|
198
|
+
details.innerHTML = `
|
|
199
|
+
<div class="space-y-4">
|
|
200
|
+
<div class="border-b border-gray-200 dark:border-gray-700 pb-4">
|
|
201
|
+
<div class="flex items-center space-x-2 mb-2">
|
|
202
|
+
<span class="text-sm font-mono text-gray-500 dark:text-gray-400 transition-colors duration-200">${commit.hash}</span>
|
|
203
|
+
<span class="text-sm text-gray-600 dark:text-gray-400 transition-colors duration-200">by ${commit.author}</span>
|
|
204
|
+
<span class="text-sm text-gray-500 dark:text-gray-500 transition-colors duration-200">${this.formatDate(commit.date)}</span>
|
|
205
|
+
</div>
|
|
206
|
+
<h3 class="text-xl font-semibold text-gray-900 dark:text-white transition-colors duration-200">${commit.message}</h3>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
210
|
+
<div>
|
|
211
|
+
<h4 class="text-lg font-medium text-gray-900 dark:text-white mb-3 transition-colors duration-200">Files Changed</h4>
|
|
212
|
+
<div class="space-y-2">
|
|
213
|
+
${commit.files.map(file => `
|
|
214
|
+
<div class="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-700 rounded transition-colors duration-200">
|
|
215
|
+
<span class="text-sm text-gray-700 dark:text-gray-300 transition-colors duration-200">${file}</span>
|
|
216
|
+
<button class="text-blue-500 hover:text-blue-600 text-sm transition-colors duration-200" onclick="app.showFileDiff('${file}')">
|
|
217
|
+
View Diff
|
|
218
|
+
</button>
|
|
219
|
+
</div>
|
|
220
|
+
`).join('')}
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div>
|
|
225
|
+
<h4 class="text-lg font-medium text-gray-900 dark:text-white mb-3 transition-colors duration-200">Diff Summary</h4>
|
|
226
|
+
<div class="space-y-2">
|
|
227
|
+
${diff.map(file => `
|
|
228
|
+
<div class="p-2 bg-gray-50 dark:bg-gray-700 rounded transition-colors duration-200">
|
|
229
|
+
<div class="text-sm font-medium text-gray-700 dark:text-gray-300 transition-colors duration-200">${file.file}</div>
|
|
230
|
+
<div class="text-xs text-gray-500 dark:text-gray-500 transition-colors duration-200">
|
|
231
|
+
+${file.additions} -${file.deletions}
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
`).join('')}
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
`;
|
|
240
|
+
|
|
241
|
+
modal.classList.remove('hidden');
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error('Error loading commit details:', error);
|
|
244
|
+
this.showError('Failed to load commit details');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
closeModal() {
|
|
249
|
+
document.getElementById('commitModal').classList.add('hidden');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
switchView(view) {
|
|
253
|
+
this.currentView = view;
|
|
254
|
+
|
|
255
|
+
// Update button styles with transition classes
|
|
256
|
+
document.getElementById('graphView').className = view === 'graph'
|
|
257
|
+
? 'px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200'
|
|
258
|
+
: 'px-3 py-1 text-sm bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors duration-200';
|
|
259
|
+
|
|
260
|
+
document.getElementById('listView').className = view === 'list'
|
|
261
|
+
? 'px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200'
|
|
262
|
+
: 'px-3 py-1 text-sm bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors duration-200';
|
|
263
|
+
|
|
264
|
+
// Show/hide views
|
|
265
|
+
document.getElementById('graphView').classList.toggle('hidden', view !== 'graph');
|
|
266
|
+
document.getElementById('listView').classList.toggle('hidden', view !== 'list');
|
|
267
|
+
|
|
268
|
+
this.renderCommits();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
toggleDarkMode() {
|
|
272
|
+
this.darkMode = !this.darkMode;
|
|
273
|
+
document.documentElement.classList.toggle('dark', this.darkMode);
|
|
274
|
+
document.body.classList.toggle('dark', this.darkMode);
|
|
275
|
+
|
|
276
|
+
// Save preference to localStorage
|
|
277
|
+
localStorage.setItem('darkMode', this.darkMode.toString());
|
|
278
|
+
|
|
279
|
+
const button = document.getElementById('darkMode');
|
|
280
|
+
button.textContent = this.darkMode ? '☀️' : '🌙';
|
|
281
|
+
|
|
282
|
+
// Force re-render to ensure all elements get updated dark mode styles
|
|
283
|
+
this.renderCommits();
|
|
284
|
+
|
|
285
|
+
// If currently in graph view, re-render the graph with new colors
|
|
286
|
+
if (this.currentView === 'graph') {
|
|
287
|
+
this.renderGraphView();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
handleSearch(query) {
|
|
292
|
+
const filtered = this.commits.filter(commit =>
|
|
293
|
+
commit.message.toLowerCase().includes(query.toLowerCase()) ||
|
|
294
|
+
commit.author.toLowerCase().includes(query.toLowerCase()) ||
|
|
295
|
+
commit.hash.toLowerCase().includes(query.toLowerCase())
|
|
296
|
+
);
|
|
297
|
+
this.renderFilteredCommits(filtered);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
handleAuthorFilter(author) {
|
|
301
|
+
if (!author) {
|
|
302
|
+
this.renderCommits();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const filtered = this.commits.filter(commit => commit.author === author);
|
|
306
|
+
this.renderFilteredCommits(filtered);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
handleSinceFilter(since) {
|
|
310
|
+
if (!since) {
|
|
311
|
+
this.renderCommits();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const filtered = this.commits.filter(commit => new Date(commit.date) >= new Date(since));
|
|
315
|
+
this.renderFilteredCommits(filtered);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
handleFileFilter(file) {
|
|
319
|
+
if (!file) {
|
|
320
|
+
this.renderCommits();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const filtered = this.commits.filter(commit =>
|
|
324
|
+
commit.files.some(f => f.toLowerCase().includes(file.toLowerCase()))
|
|
325
|
+
);
|
|
326
|
+
this.renderFilteredCommits(filtered);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
renderFilteredCommits(filtered) {
|
|
330
|
+
if (this.currentView === 'list') {
|
|
331
|
+
const container = document.getElementById('commitsList');
|
|
332
|
+
container.innerHTML = '';
|
|
333
|
+
filtered.forEach(commit => {
|
|
334
|
+
const commitElement = this.createCommitElement(commit);
|
|
335
|
+
container.appendChild(commitElement);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
populateFilters() {
|
|
341
|
+
const authors = [...new Set(this.commits.map(c => c.author))];
|
|
342
|
+
const authorSelect = document.getElementById('authorFilter');
|
|
343
|
+
authorSelect.innerHTML = '<option value="">All authors</option>';
|
|
344
|
+
authors.forEach(author => {
|
|
345
|
+
const option = document.createElement('option');
|
|
346
|
+
option.value = author;
|
|
347
|
+
option.textContent = author;
|
|
348
|
+
authorSelect.appendChild(option);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
formatDate(dateString) {
|
|
353
|
+
const date = new Date(dateString);
|
|
354
|
+
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
showError(message) {
|
|
358
|
+
// Simple error display - could be enhanced with a toast notification
|
|
359
|
+
alert(message);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
hideLoading() {
|
|
363
|
+
document.getElementById('loading').style.display = 'none';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
initializeDarkMode() {
|
|
367
|
+
// Check if user has a saved preference
|
|
368
|
+
const savedDarkMode = localStorage.getItem('darkMode');
|
|
369
|
+
if (savedDarkMode !== null) {
|
|
370
|
+
this.darkMode = savedDarkMode === 'true';
|
|
371
|
+
if (this.darkMode) {
|
|
372
|
+
document.documentElement.classList.add('dark');
|
|
373
|
+
document.body.classList.add('dark');
|
|
374
|
+
const button = document.getElementById('darkMode');
|
|
375
|
+
button.textContent = '☀️';
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
getGraphColors() {
|
|
381
|
+
if (this.darkMode) {
|
|
382
|
+
return {
|
|
383
|
+
link: '#4b5563', // gray-600 for dark mode
|
|
384
|
+
nodeFill: '#3b82f6', // blue-500 (same for both modes)
|
|
385
|
+
nodeStroke: '#1e40af', // blue-700 (same for both modes)
|
|
386
|
+
text: '#9ca3af' // gray-400 for dark mode
|
|
387
|
+
};
|
|
388
|
+
} else {
|
|
389
|
+
return {
|
|
390
|
+
link: '#cbd5e0', // gray-300 for light mode
|
|
391
|
+
nodeFill: '#3b82f6', // blue-500 (same for both modes)
|
|
392
|
+
nodeStroke: '#1e40af', // blue-700 (same for both modes)
|
|
393
|
+
text: '#6b7280' // gray-500 for light mode
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Initialize the app when the page loads
|
|
400
|
+
let app;
|
|
401
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
402
|
+
app = new GitHistoryUI();
|
|
403
|
+
});
|