|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>ContraGit App</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
|
<style> |
|
|
.diff-add { |
|
|
background-color: #dcfce7; |
|
|
color: #16a34a; |
|
|
padding: 0 2px; |
|
|
border-radius: 2px; |
|
|
display: inline; |
|
|
} |
|
|
.diff-del { |
|
|
background-color: #fee2e2; |
|
|
color: #dc2626; |
|
|
text-decoration: line-through; |
|
|
padding: 0 2px; |
|
|
border-radius: 2px; |
|
|
display: inline; |
|
|
} |
|
|
.diff-neutral { |
|
|
color: #4b5563; |
|
|
display: inline; |
|
|
} |
|
|
pre { |
|
|
white-space: pre-wrap; |
|
|
word-wrap: break-word; |
|
|
} |
|
|
.contract-content { |
|
|
max-height: 200px; |
|
|
overflow-y: auto; |
|
|
} |
|
|
.version-card { |
|
|
transition: all 0.2s ease; |
|
|
} |
|
|
.version-card:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
.comment-bubble { |
|
|
position: relative; |
|
|
background: #f3f4f6; |
|
|
border-radius: 0.5rem; |
|
|
} |
|
|
.comment-bubble:after { |
|
|
content: ''; |
|
|
position: absolute; |
|
|
top: -10px; |
|
|
left: 15px; |
|
|
border-width: 0 10px 10px; |
|
|
border-style: solid; |
|
|
border-color: #f3f4f6 transparent; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="min-h-screen bg-gray-100 font-sans"> |
|
|
<div id="app"> |
|
|
|
|
|
<header class="bg-white shadow-sm"> |
|
|
<nav class="container mx-auto px-4 sm:px-6 lg:px-8 py-3 flex items-center justify-between"> |
|
|
<h1 class="text-2xl font-bold text-blue-600 cursor-pointer" onclick="goBackToDashboard()"> |
|
|
<i class="fas fa-file-contract mr-2"></i>ContraGit App |
|
|
</h1> |
|
|
<div class="flex items-center space-x-4"> |
|
|
<button id="newContractBtn" class="hidden md:flex items-center bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition duration-200 shadow"> |
|
|
<i class="fas fa-plus mr-2"></i> New Contract |
|
|
</button> |
|
|
<div class="relative"> |
|
|
<img src="https://randomuser.me/api/portraits/men/32.jpg" alt="User" class="w-8 h-8 rounded-full cursor-pointer" id="userMenuBtn"> |
|
|
<div id="userMenu" class="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-10"> |
|
|
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Profile</a> |
|
|
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Settings</a> |
|
|
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Logout</a> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</nav> |
|
|
</header> |
|
|
|
|
|
|
|
|
<main class="container mx-auto px-4 sm:px-6 lg:px-8 py-8"> |
|
|
<div id="dashboardView"> |
|
|
|
|
|
</div> |
|
|
</main> |
|
|
|
|
|
|
|
|
<footer class="bg-gray-200 text-center py-4 mt-12"> |
|
|
<p class="text-sm text-gray-600">© 2025 ContraGit App</p> |
|
|
</footer> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
// Mock Data |
|
|
const MOCK_CONTRACTS = [ |
|
|
{ |
|
|
id: 1, |
|
|
name: "Freelance Agreement - Alex & Client X", |
|
|
createdAt: "2023-10-01T10:00:00Z", |
|
|
updatedAt: "2023-10-01T11:00:00Z", |
|
|
}, |
|
|
{ |
|
|
id: 2, |
|
|
name: "NDA - Project Phoenix", |
|
|
createdAt: "2023-09-15T14:30:00Z", |
|
|
updatedAt: "2023-09-15T14:30:00Z", |
|
|
}, |
|
|
]; |
|
|
|
|
|
const MOCK_VERSIONS = { |
|
|
1: [ |
|
|
{ id: 1, contractId: 1, content: "Payment: $500 due on completion.\nDeadline: October 30th.", createdAt: "2023-10-01T10:00:00Z", createdBy: "Alex" }, |
|
|
{ id: 2, contractId: 1, content: "Payment: $600 due in two installments: $300 on start, $300 on completion.\nDeadline: October 30th.", createdAt: "2023-10-01T11:00:00Z", createdBy: "Alex" }, |
|
|
{ id: 3, contractId: 1, content: "Payment: $600 due in two installments: $300 on start, $300 on completion.\nDeadline: November 5th.", createdAt: "2023-10-01T12:15:00Z", createdBy: "Jamie" }, |
|
|
], |
|
|
2: [ |
|
|
{ id: 4, contractId: 2, content: "Standard Non-Disclosure Agreement terms apply.", createdAt: "2023-09-15T14:30:00Z", createdBy: "System" }, |
|
|
], |
|
|
}; |
|
|
|
|
|
const MOCK_COMMENTS = { |
|
|
2: [ |
|
|
{ id: 1, versionId: 2, userId: "Jamie", comment: "I agree with the new payment terms but need to clarify the deadline.", createdAt: "2023-10-01T12:00:00Z" }, |
|
|
], |
|
|
3: [ |
|
|
{ id: 2, versionId: 3, userId: "Alex", comment: "Deadline updated to Nov 5th. Please confirm.", createdAt: "2023-10-01T12:20:00Z" }, |
|
|
] |
|
|
}; |
|
|
|
|
|
// State |
|
|
let currentView = 'dashboard'; |
|
|
let selectedContractId = null; |
|
|
let compareVersionIds = { v1: null, v2: null }; |
|
|
let contracts = []; |
|
|
let versions = {}; |
|
|
let comments = {}; |
|
|
let loading = true; |
|
|
let error = null; |
|
|
|
|
|
// Utility Functions |
|
|
function formatDate(isoString) { |
|
|
if (!isoString) return 'N/A'; |
|
|
try { |
|
|
const date = new Date(isoString); |
|
|
return date.toLocaleString('en-US', { |
|
|
month: 'short', |
|
|
day: 'numeric', |
|
|
year: 'numeric', |
|
|
hour: '2-digit', |
|
|
minute: '2-digit' |
|
|
}); |
|
|
} catch (e) { |
|
|
return 'Invalid Date'; |
|
|
} |
|
|
} |
|
|
|
|
|
function calculateDiff(text1, text2) { |
|
|
// This is a simplified diff algorithm - in a real app, use a proper diff library |
|
|
const lines1 = text1.split('\n'); |
|
|
const lines2 = text2.split('\n'); |
|
|
const maxLength = Math.max(lines1.length, lines2.length); |
|
|
let result = []; |
|
|
|
|
|
for (let i = 0; i < maxLength; i++) { |
|
|
const line1 = lines1[i] || ''; |
|
|
const line2 = lines2[i] || ''; |
|
|
|
|
|
if (line1 === line2) { |
|
|
result.push({ |
|
|
value: line1, |
|
|
added: false, |
|
|
removed: false |
|
|
}); |
|
|
} else { |
|
|
if (line1) { |
|
|
result.push({ |
|
|
value: line1, |
|
|
added: false, |
|
|
removed: true |
|
|
}); |
|
|
} |
|
|
if (line2) { |
|
|
result.push({ |
|
|
value: line2, |
|
|
added: true, |
|
|
removed: false |
|
|
}); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return result; |
|
|
} |
|
|
|
|
|
// Navigation Functions |
|
|
function viewContract(id) { |
|
|
selectedContractId = id; |
|
|
currentView = 'contract'; |
|
|
renderView(); |
|
|
} |
|
|
|
|
|
function goBackToDashboard() { |
|
|
selectedContractId = null; |
|
|
currentView = 'dashboard'; |
|
|
renderView(); |
|
|
} |
|
|
|
|
|
function goBackToContract() { |
|
|
if (selectedContractId) { |
|
|
currentView = 'contract'; |
|
|
} else { |
|
|
goBackToDashboard(); |
|
|
} |
|
|
renderView(); |
|
|
} |
|
|
|
|
|
function goToAddVersion() { |
|
|
currentView = 'addVersion'; |
|
|
renderView(); |
|
|
} |
|
|
|
|
|
function goToCompareVersions(v1Id = null, v2Id = null) { |
|
|
const contractVersions = versions[selectedContractId] || []; |
|
|
const defaultV1 = v1Id ?? (contractVersions.length > 1 ? contractVersions[contractVersions.length - 2].id : null); |
|
|
const defaultV2 = v2Id ?? (contractVersions.length > 0 ? contractVersions[contractVersions.length - 1].id : null); |
|
|
compareVersionIds = { v1: defaultV1, v2: defaultV2 }; |
|
|
currentView = 'compare'; |
|
|
renderView(); |
|
|
} |
|
|
|
|
|
function goToNewContract() { |
|
|
currentView = 'newContract'; |
|
|
renderView(); |
|
|
} |
|
|
|
|
|
// Data Manipulation Functions |
|
|
function handleAddContract(name) { |
|
|
const newId = Math.max(0, ...contracts.map(c => c.id)) + 1; |
|
|
const newContract = { |
|
|
id: newId, |
|
|
name: name, |
|
|
createdAt: new Date().toISOString(), |
|
|
updatedAt: new Date().toISOString(), |
|
|
}; |
|
|
contracts = [...contracts, newContract]; |
|
|
versions = { ...versions, [newId]: [] }; |
|
|
|
|
|
const initialVersion = { |
|
|
id: Math.max(0, ...Object.values(versions).flat().map(v => v.id)) + 1, |
|
|
contractId: newId, |
|
|
content: `Initial content for ${name}. Please edit.`, |
|
|
createdAt: new Date().toISOString(), |
|
|
createdBy: "System", |
|
|
}; |
|
|
versions = { ...versions, [newId]: [initialVersion] }; |
|
|
|
|
|
alert(`Contract '${name}' created successfully.`); |
|
|
viewContract(newId); |
|
|
} |
|
|
|
|
|
function handleAddVersion(contractId, content, createdBy) { |
|
|
if (!versions[contractId]) { |
|
|
alert("Error: Contract not found."); |
|
|
return; |
|
|
} |
|
|
|
|
|
const newVersionId = Math.max(0, ...Object.values(versions).flat().map(v => v.id)) + 1; |
|
|
const newVersion = { |
|
|
id: newVersionId, |
|
|
contractId: contractId, |
|
|
content: content, |
|
|
createdAt: new Date().toISOString(), |
|
|
createdBy: createdBy || "Unknown User", |
|
|
}; |
|
|
|
|
|
versions = { |
|
|
...versions, |
|
|
[contractId]: [...versions[contractId], newVersion], |
|
|
}; |
|
|
|
|
|
contracts = contracts.map(c => |
|
|
c.id === contractId ? { ...c, updatedAt: new Date().toISOString() } : c |
|
|
); |
|
|
|
|
|
alert(`Version ${versions[contractId].length} added successfully.`); |
|
|
goBackToContract(); |
|
|
} |
|
|
|
|
|
function handleAddComment(versionId, comment, userId) { |
|
|
const newCommentId = Math.max(0, ...Object.values(comments).flat().map(c => c.id)) + 1; |
|
|
const newComment = { |
|
|
id: newCommentId, |
|
|
versionId: versionId, |
|
|
userId: userId || "Guest", |
|
|
comment: comment, |
|
|
createdAt: new Date().toISOString(), |
|
|
}; |
|
|
|
|
|
comments = { |
|
|
...comments, |
|
|
[versionId]: [...(comments[versionId] || []), newComment], |
|
|
}; |
|
|
|
|
|
renderView(); |
|
|
} |
|
|
|
|
|
function handleFinalizeContract(contractId) { |
|
|
const contract = contracts.find(c => c.id === contractId); |
|
|
if (contract) { |
|
|
alert(`Contract '${contract.name}' finalized successfully.`); |
|
|
} |
|
|
} |
|
|
|
|
|
// View Components |
|
|
function renderDashboard() { |
|
|
return ` |
|
|
<div> |
|
|
<div class="flex justify-between items-center mb-6"> |
|
|
<h2 class="text-3xl font-bold text-gray-800">Contracts Dashboard</h2> |
|
|
<button |
|
|
onclick="goToNewContract()" |
|
|
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition duration-200 shadow flex items-center" |
|
|
> |
|
|
<i class="fas fa-plus mr-2"></i> New Contract |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
${contracts.length === 0 ? ` |
|
|
<div class="bg-white p-8 rounded-lg shadow text-center"> |
|
|
<i class="fas fa-file-alt text-4xl text-gray-400 mb-4"></i> |
|
|
<p class="text-gray-500 mb-4">No contracts found. Create one to get started!</p> |
|
|
<button |
|
|
onclick="goToNewContract()" |
|
|
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition duration-200 shadow" |
|
|
> |
|
|
Create First Contract |
|
|
</button> |
|
|
</div> |
|
|
` : ` |
|
|
<div class="bg-white shadow overflow-hidden sm:rounded-md"> |
|
|
<ul class="divide-y divide-gray-200"> |
|
|
${contracts.map(contract => ` |
|
|
<li key="${contract.id}" class="version-card"> |
|
|
<button |
|
|
onclick="viewContract(${contract.id})" |
|
|
class="block hover:bg-gray-50 w-full text-left px-4 py-4 sm:px-6 transition duration-150" |
|
|
> |
|
|
<div class="flex items-center justify-between"> |
|
|
<p class="text-lg font-medium text-blue-600 truncate"> |
|
|
<i class="fas fa-file-contract mr-2"></i>${contract.name} |
|
|
</p> |
|
|
<div class="ml-2 flex-shrink-0 flex"> |
|
|
<p class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"> |
|
|
Active |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
<div class="mt-2 sm:flex sm:justify-between"> |
|
|
<div class="sm:flex"> |
|
|
<p class="flex items-center text-sm text-gray-500"> |
|
|
<i class="fas fa-calendar-alt mr-2 text-gray-400"></i> |
|
|
Created: ${formatDate(contract.createdAt)} |
|
|
</p> |
|
|
<p class="mt-2 flex items-center text-sm text-gray-500 sm:mt-0 sm:ml-6"> |
|
|
<i class="fas fa-clock mr-2 text-gray-400"></i> |
|
|
Last Updated: ${formatDate(contract.updatedAt)} |
|
|
</p> |
|
|
</div> |
|
|
<div class="mt-2 flex items-center text-sm text-gray-500 sm:mt-0"> |
|
|
<i class="fas fa-chevron-right text-gray-400"></i> |
|
|
</div> |
|
|
</div> |
|
|
</button> |
|
|
</li> |
|
|
`).join('')} |
|
|
</ul> |
|
|
</div> |
|
|
`} |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function renderNewContract() { |
|
|
return ` |
|
|
<div class="bg-white p-6 rounded-lg shadow-md max-w-lg mx-auto"> |
|
|
<h2 class="text-2xl font-bold text-gray-800 mb-6">Create New Contract</h2> |
|
|
<form onsubmit="event.preventDefault(); handleAddContract(document.getElementById('contractName').value);"> |
|
|
<div class="mb-4"> |
|
|
<label for="contractName" class="block text-sm font-medium text-gray-700 mb-1"> |
|
|
Contract Name |
|
|
</label> |
|
|
<input |
|
|
type="text" |
|
|
id="contractName" |
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" |
|
|
placeholder="e.g., Freelance Agreement - Alex & Client X" |
|
|
required |
|
|
> |
|
|
</div> |
|
|
|
|
|
<div class="mb-6"> |
|
|
<label for="contractFile" class="block text-sm font-medium text-gray-700 mb-1"> |
|
|
Upload Initial Document (Optional) |
|
|
</label> |
|
|
<div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md"> |
|
|
<div class="space-y-1 text-center"> |
|
|
<i class="fas fa-file-upload text-4xl text-gray-400 mx-auto"></i> |
|
|
<div class="flex text-sm text-gray-600 justify-center"> |
|
|
<label class="relative cursor-pointer bg-white rounded-md font-medium text-blue-600 hover:text-blue-500"> |
|
|
<span>Upload a file</span> |
|
|
<input id="contractFile" name="contractFile" type="file" class="sr-only"> |
|
|
</label> |
|
|
<p class="pl-1">or drag and drop</p> |
|
|
</div> |
|
|
<p class="text-xs text-gray-500">PDF, DOC, DOCX, TXT up to 10MB</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="flex justify-end space-x-3"> |
|
|
<button |
|
|
type="button" |
|
|
onclick="goBackToDashboard()" |
|
|
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-300 transition duration-200" |
|
|
> |
|
|
Cancel |
|
|
</button> |
|
|
<button |
|
|
type="submit" |
|
|
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition duration-200 shadow" |
|
|
> |
|
|
Create Contract |
|
|
</button> |
|
|
</div> |
|
|
</form> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function renderContractDetail() { |
|
|
const contract = contracts.find(c => c.id === selectedContractId); |
|
|
if (!contract) { |
|
|
return ` |
|
|
<div class="bg-white p-6 rounded-lg shadow text-center"> |
|
|
<i class="fas fa-exclamation-triangle text-4xl text-yellow-500 mb-4"></i> |
|
|
<p class="text-gray-700 mb-4">Contract not found.</p> |
|
|
<button onclick="goBackToDashboard()" class="text-blue-600 hover:underline"> |
|
|
<i class="fas fa-arrow-left mr-2"></i>Go Back to Dashboard |
|
|
</button> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
const contractVersions = versions[selectedContractId] || []; |
|
|
|
|
|
return ` |
|
|
<div> |
|
|
<button onclick="goBackToDashboard()" class="mb-6 text-blue-600 hover:underline flex items-center"> |
|
|
<i class="fas fa-arrow-left mr-2"></i> Back to Dashboard |
|
|
</button> |
|
|
|
|
|
<div class="flex justify-between items-start mb-6"> |
|
|
<div> |
|
|
<h2 class="text-3xl font-bold text-gray-800 mb-1">${contract.name}</h2> |
|
|
<p class="text-sm text-gray-500"> |
|
|
<i class="fas fa-clock mr-1"></i> Last Updated: ${formatDate(contract.updatedAt)} |
|
|
</p> |
|
|
</div> |
|
|
<div class="flex space-x-2"> |
|
|
<button |
|
|
onclick="handleFinalizeContract(${contract.id})" |
|
|
class="bg-red-600 text-white px-3 py-1 rounded-lg hover:bg-red-700 transition duration-200 shadow text-sm" |
|
|
title="Finalize Contract" |
|
|
> |
|
|
<i class="fas fa-lock mr-1"></i> Finalize |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="flex flex-wrap gap-3 mb-6"> |
|
|
<button |
|
|
onclick="goToAddVersion()" |
|
|
class="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition duration-200 shadow flex items-center" |
|
|
> |
|
|
<i class="fas fa-plus mr-2"></i> Add New Version |
|
|
</button> |
|
|
<button |
|
|
onclick="goToCompareVersions()" |
|
|
disabled="${contractVersions.length < 2}" |
|
|
class="${contractVersions.length < 2 ? 'opacity-50 cursor-not-allowed' : ''} bg-yellow-500 text-white px-4 py-2 rounded-lg hover:bg-yellow-600 transition duration-200 shadow flex items-center" |
|
|
> |
|
|
<i class="fas fa-code-compare mr-2"></i> Compare Versions |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
<h3 class="text-2xl font-semibold text-gray-700 mb-4 flex items-center"> |
|
|
<i class="fas fa-history mr-2"></i> Version History |
|
|
</h3> |
|
|
|
|
|
${contractVersions.length === 0 ? ` |
|
|
<div class="bg-white p-6 rounded-lg shadow text-center"> |
|
|
<i class="fas fa-file-alt text-4xl text-gray-400 mb-4"></i> |
|
|
<p class="text-gray-500">No versions yet. Add the first version to get started!</p> |
|
|
</div> |
|
|
` : ` |
|
|
<div class="space-y-4"> |
|
|
${contractVersions.slice().reverse().map((version, index) => { |
|
|
const versionNumber = contractVersions.length - index; |
|
|
const versionComments = comments[version.id] || []; |
|
|
const isCommenting = localStorage.getItem('commentingVersionId') === String(version.id); |
|
|
|
|
|
return ` |
|
|
<div class="bg-white p-4 rounded-lg shadow border border-gray-200 version-card"> |
|
|
<div class="flex justify-between items-center mb-3"> |
|
|
<h4 class="text-lg font-semibold flex items-center"> |
|
|
<i class="fas fa-code-branch mr-2 text-blue-500"></i> Version ${versionNumber} |
|
|
</h4> |
|
|
<span class="text-sm text-gray-500"> |
|
|
By ${version.createdBy} at ${formatDate(version.createdAt)} |
|
|
</span> |
|
|
</div> |
|
|
|
|
|
<div class="contract-content bg-gray-50 p-3 rounded mb-3"> |
|
|
<pre class="text-sm text-gray-700 whitespace-pre-wrap font-mono">${version.content}</pre> |
|
|
</div> |
|
|
|
|
|
<div class="mt-3 pt-3 border-t border-gray-200"> |
|
|
<h5 class="text-sm font-semibold text-gray-600 mb-2 flex items-center"> |
|
|
<i class="fas fa-comments mr-2"></i> Comments (${versionComments.length}) |
|
|
</h5> |
|
|
|
|
|
${versionComments.length > 0 ? ` |
|
|
<ul class="space-y-2 mb-3"> |
|
|
${versionComments.map(comment => ` |
|
|
<li class="comment-bubble p-3"> |
|
|
<div class="flex justify-between items-start"> |
|
|
<span class="font-semibold text-sm">${comment.userId}:</span> |
|
|
<span class="text-xs text-gray-400">${formatDate(comment.createdAt)}</span> |
|
|
</div> |
|
|
<p class="text-sm mt-1">${comment.comment}</p> |
|
|
</li> |
|
|
`).join('')} |
|
|
</ul> |
|
|
` : ''} |
|
|
|
|
|
${isCommenting ? ` |
|
|
<form onsubmit="event.preventDefault(); handleAddComment(${version.id}, document.getElementById('comment-${version.id}').value, 'Alex'); localStorage.removeItem('commentingVersionId');"> |
|
|
<textarea |
|
|
id="comment-${version.id}" |
|
|
rows="2" |
|
|
placeholder="Add a comment to Version ${versionNumber}..." |
|
|
class="w-full p-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500" |
|
|
required |
|
|
></textarea> |
|
|
<div class="flex justify-end space-x-2 mt-1"> |
|
|
<button type="button" onclick="localStorage.removeItem('commentingVersionId'); renderView();" class="text-xs text-gray-500 hover:underline">Cancel</button> |
|
|
<button type="submit" class="bg-blue-500 text-white px-2 py-1 rounded text-xs hover:bg-blue-600">Post</button> |
|
|
</div> |
|
|
</form> |
|
|
` : ` |
|
|
<button onclick="localStorage.setItem('commentingVersionId', '${version.id}'); renderView();" class="text-xs text-blue-600 hover:underline flex items-center"> |
|
|
<i class="fas fa-plus mr-1"></i> Add Comment |
|
|
</button> |
|
|
`} |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
}).join('')} |
|
|
</div> |
|
|
`} |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function renderAddVersion() { |
|
|
const baseVersion = versions[selectedContractId]?.[versions[selectedContractId].length - 1]; |
|
|
|
|
|
return ` |
|
|
<div class="bg-white p-6 rounded-lg shadow-md max-w-3xl mx-auto"> |
|
|
<button onclick="goBackToContract()" class="mb-4 text-sm text-blue-600 hover:underline flex items-center"> |
|
|
<i class="fas fa-arrow-left mr-1"></i> Cancel and Back to Contract |
|
|
</button> |
|
|
|
|
|
<h2 class="text-2xl font-bold text-gray-800 mb-4">Add New Version</h2> |
|
|
<p class="text-sm text-gray-500 mb-4"> |
|
|
<i class="fas fa-info-circle mr-1"></i> Editing based on ${baseVersion ? `Version ID ${baseVersion.id}` : 'N/A'} |
|
|
</p> |
|
|
|
|
|
<form onsubmit="event.preventDefault(); handleAddVersion(${selectedContractId}, document.getElementById('versionContent').value, document.getElementById('createdBy').value);"> |
|
|
<div class="mb-4"> |
|
|
<label for="versionContent" class="block text-sm font-medium text-gray-700 mb-1"> |
|
|
Contract Content |
|
|
</label> |
|
|
<textarea |
|
|
id="versionContent" |
|
|
rows="15" |
|
|
class="w-full p-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 font-mono text-sm" |
|
|
placeholder="Enter contract text here..." |
|
|
required |
|
|
>${baseVersion?.content || ''}</textarea> |
|
|
</div> |
|
|
|
|
|
<div class="mb-6"> |
|
|
<label for="createdBy" class="block text-sm font-medium text-gray-700 mb-1"> |
|
|
Your Name (Editor) |
|
|
</label> |
|
|
<input |
|
|
type="text" |
|
|
id="createdBy" |
|
|
value="Alex" |
|
|
class="w-full sm:w-1/2 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" |
|
|
required |
|
|
> |
|
|
</div> |
|
|
|
|
|
<div class="flex justify-end space-x-3"> |
|
|
<button |
|
|
type="button" |
|
|
onclick="goBackToContract()" |
|
|
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-300 transition duration-200" |
|
|
> |
|
|
Cancel |
|
|
</button> |
|
|
<button |
|
|
type="submit" |
|
|
class="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition duration-200 shadow" |
|
|
> |
|
|
<i class="fas fa-save mr-2"></i> Save New Version |
|
|
</button> |
|
|
</div> |
|
|
</form> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function renderCompareVersions() { |
|
|
const contractVersions = versions[selectedContractId] || []; |
|
|
const version1 = contractVersions.find(v => v.id === parseInt(compareVersionIds.v1)); |
|
|
const version2 = contractVersions.find(v => v.id === parseInt(compareVersionIds.v2)); |
|
|
|
|
|
const diffResult = version1 && version2 ? calculateDiff(version1.content, version2.content) : []; |
|
|
|
|
|
return ` |
|
|
<div> |
|
|
<button onclick="goBackToContract()" class="mb-6 text-blue-600 hover:underline flex items-center"> |
|
|
<i class="fas fa-arrow-left mr-2"></i> Back to Contract |
|
|
</button> |
|
|
|
|
|
<h2 class="text-3xl font-bold text-gray-800 mb-6 flex items-center"> |
|
|
<i class="fas fa-code-compare mr-3"></i> Compare Versions |
|
|
</h2> |
|
|
|
|
|
${contractVersions.length < 2 ? ` |
|
|
<div class="bg-white p-6 rounded-lg shadow text-center"> |
|
|
<i class="fas fa-exclamation-circle text-4xl text-yellow-500 mb-4"></i> |
|
|
<p class="text-gray-700 mb-4">You need at least two versions to compare.</p> |
|
|
<button onclick="goToAddVersion()" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition duration-200 shadow"> |
|
|
<i class="fas fa-plus mr-2"></i> Add Version |
|
|
</button> |
|
|
</div> |
|
|
` : ` |
|
|
<div class="flex space-x-4 mb-6 bg-white p-4 rounded-lg shadow border border-gray-200 items-center"> |
|
|
<div class="flex-1"> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Compare Version:</label> |
|
|
<select |
|
|
onchange="compareVersionIds.v1 = this.value; renderView();" |
|
|
class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" |
|
|
value="${compareVersionIds.v1 || ''}" |
|
|
> |
|
|
<option value="" disabled>Select Version</option> |
|
|
${contractVersions.map((v, index) => ` |
|
|
<option value="${v.id}" ${v.id === parseInt(compareVersionIds.v1) ? 'selected' : ''}> |
|
|
Version ${index + 1} (${formatDate(v.createdAt)}) |
|
|
</option> |
|
|
`).join('')} |
|
|
</select> |
|
|
</div> |
|
|
<span class="mt-6 text-gray-500"> |
|
|
<i class="fas fa-arrows-left-right"></i> |
|
|
</span> |
|
|
<div class="flex-1"> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">With Version:</label> |
|
|
<select |
|
|
onchange="compareVersionIds.v2 = this.value; renderView();" |
|
|
class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" |
|
|
value="${compareVersionIds.v2 || ''}" |
|
|
> |
|
|
<option value="" disabled>Select Version</option> |
|
|
${contractVersions.map((v, index) => ` |
|
|
<option value="${v.id}" ${v.id === parseInt(compareVersionIds.v2) ? 'selected' : ''}> |
|
|
Version ${index + 1} (${formatDate(v.createdAt)}) |
|
|
</option> |
|
|
`).join('')} |
|
|
</select> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
${(version1 && version2) ? ` |
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 font-mono text-sm bg-white rounded p-4 shadow border border-gray-300"> |
|
|
<div> |
|
|
<h4 class="font-semibold mb-2 border-b pb-1"> |
|
|
Version ${contractVersions.findIndex(v => v.id === version1.id) + 1} |
|
|
<span class="text-xs font-normal text-gray-500 ml-2"> |
|
|
(${formatDate(version1.createdAt)} by ${version1.createdBy}) |
|
|
</span> |
|
|
</h4> |
|
|
<pre class="whitespace-pre-wrap contract-content">${version1.content}</pre> |
|
|
</div> |
|
|
<div> |
|
|
<h4 class="font-semibold mb-2 border-b pb-1"> |
|
|
Version ${contractVersions.findIndex(v => v.id === version2.id) + 1} |
|
|
<span class="text-xs font-normal text-gray-500 ml-2"> |
|
|
(${formatDate(version2.createdAt)} by ${version2.createdBy}) |
|
|
</span> |
|
|
</h4> |
|
|
<pre class="whitespace-pre-wrap contract-content">${version2.content}</pre> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="mt-8 bg-white p-4 rounded shadow border font-mono text-sm"> |
|
|
<h4 class="font-semibold mb-2 border-b pb-1">Changes Summary</h4> |
|
|
<div class="whitespace-pre-wrap"> |
|
|
${diffResult.map(part => { |
|
|
if (part.added) { |
|
|
return `<span class="diff-add">${part.value}</span>`; |
|
|
} else if (part.removed) { |
|
|
return `<span class="diff-del">${part.value}</span>`; |
|
|
} else { |
|
|
return `<span class="diff-neutral">${part.value}</span>`; |
|
|
} |
|
|
}).join('')} |
|
|
</div> |
|
|
</div> |
|
|
` : ` |
|
|
<div class="bg-white p-6 rounded-lg shadow text-center"> |
|
|
<i class="fas fa-info-circle text-4xl text-blue-500 mb-4"></i> |
|
|
<p class="text-gray-700">Please select two versions to compare.</p> |
|
|
</div> |
|
|
`} |
|
|
`} |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
// Main Render Function |
|
|
function renderView() { |
|
|
let viewContent = ''; |
|
|
|
|
|
if (loading) { |
|
|
viewContent = ` |
|
|
<div class="text-center p-10"> |
|
|
<i class="fas fa-spinner fa-spin text-4xl text-blue-500 mb-4"></i> |
|
|
<p>Loading...</p> |
|
|
</div> |
|
|
`; |
|
|
} else if (error) { |
|
|
viewContent = ` |
|
|
<div class="text-center p-10 text-red-600"> |
|
|
<i class="fas fa-exclamation-triangle text-4xl mb-4"></i> |
|
|
<p>${error}</p> |
|
|
</div> |
|
|
`; |
|
|
} else { |
|
|
switch (currentView) { |
|
|
case 'contract': |
|
|
viewContent = renderContractDetail(); |
|
|
break; |
|
|
case 'compare': |
|
|
viewContent = renderCompareVersions(); |
|
|
break; |
|
|
case 'addVersion': |
|
|
viewContent = renderAddVersion(); |
|
|
break; |
|
|
case 'newContract': |
|
|
viewContent = renderNewContract(); |
|
|
break; |
|
|
case 'dashboard': |
|
|
default: |
|
|
viewContent = renderDashboard(); |
|
|
} |
|
|
} |
|
|
|
|
|
document.getElementById('dashboardView').innerHTML = viewContent; |
|
|
|
|
|
// Update active button in header |
|
|
document.getElementById('newContractBtn').classList.toggle('hidden', currentView !== 'dashboard'); |
|
|
} |
|
|
|
|
|
// Initialize App |
|
|
function initApp() { |
|
|
// Simulate API Fetch |
|
|
loading = true; |
|
|
setTimeout(() => { |
|
|
try { |
|
|
contracts = MOCK_CONTRACTS; |
|
|
versions = MOCK_VERSIONS; |
|
|
comments = MOCK_COMMENTS; |
|
|
loading = false; |
|
|
renderView(); |
|
|
} catch (err) { |
|
|
error = "Failed to load data."; |
|
|
loading = false; |
|
|
console.error(err); |
|
|
renderView(); |
|
|
} |
|
|
}, 500); |
|
|
|
|
|
// Setup event listeners |
|
|
document.getElementById('userMenuBtn').addEventListener('click', function() { |
|
|
document.getElementById('userMenu').classList.toggle('hidden'); |
|
|
}); |
|
|
|
|
|
document.getElementById('newContractBtn').addEventListener('click', goToNewContract); |
|
|
|
|
|
// Close user menu when clicking outside |
|
|
document.addEventListener('click', function(event) { |
|
|
if (!event.target.closest('#userMenu') && !event.target.closest('#userMenuBtn')) { |
|
|
document.getElementById('userMenu').classList.add('hidden'); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
// Start the app when DOM is loaded |
|
|
document.addEventListener('DOMContentLoaded', initApp); |
|
|
</script> |
|
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=manideepreddym/contractverse" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
|
</html> |