working beautifully

This commit is contained in:
GuilhermeStrice
2025-06-26 04:36:18 +01:00
parent b1c2f18edd
commit bc8e4f2eda
4 changed files with 883 additions and 63 deletions

0
README Normal file
View File

View File

@ -3,13 +3,25 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VCard Viewer</title> <title>VCard Utility</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
</head> </head>
<body> <body>
<h1>VCard Viewer</h1> <div class="main-container">
<input type="file" id="vcardFile" accept=".vcf"> <div class="tab-navigation">
<button id="parserTabButton" class="tab-button active-tab-button">Parser</button>
<button id="generatorTabButton" class="tab-button">Generator</button>
<button id="privacyTabButton" class="tab-button">Privacy Policy</button>
</div>
<div id="parserView" class="tab-pane active-pane">
<h1>VCard Parser</h1>
<div class="tab-description">
<p>A vCard (or .vcf file) is a standard file format for electronic business cards. vCards allow you to easily create and share contact information that can be imported into various email clients, address books, and mobile devices.</p>
<p>This Parser section helps you upload an existing vCard (.vcf) file to view its contents in a structured format and explore its details.</p>
</div>
<label for="vcardFile" class="file-upload-label">Choose VCard File (.vcf)</label>
<input type="file" id="vcardFile" accept=".vcf">
<h2>Contact Details</h2> <h2>Contact Details</h2>
<table id="contactsTable"> <table id="contactsTable">
<thead> <thead>
@ -97,6 +109,169 @@
</div> </div>
</div> </div>
<script src="https://unpkg.com/libphonenumber-js@1.10.58/bundle/libphonenumber-min.js"></script>
<script src="script.js"></script> <script src="script.js"></script>
</div> <!-- End of parserView -->
<div id="generatorView" class="tab-pane">
<h1>VCard Generator</h1>
<div class="tab-description">
<p>A vCard (or .vcf file) is a standard file format for electronic business cards. vCards allow you to easily create and share contact information that can be imported into various email clients, address books, and mobile devices.</p>
<p>This Generator section allows you to create a new vCard by filling in the fields below. You can then download the generated .vcf file to share or import into your contact applications.</p>
</div>
<form id="vcardGeneratorForm">
<fieldset>
<legend>Name</legend>
<div><label for="genFN">Formatted Name (FN):</label><input type="text" id="genFN" name="genFN" required></div>
<div><label for="genFamilyName">Family Name:</label><input type="text" id="genFamilyName" name="genFamilyName"></div>
<div><label for="genGivenName">Given Name:</label><input type="text" id="genGivenName" name="genGivenName"></div>
<div><label for="genMiddleName">Middle Name:</label><input type="text" id="genMiddleName" name="genMiddleName"></div>
<div><label for="genPrefix">Prefix (e.g., Mr., Dr.):</label><input type="text" id="genPrefix" name="genPrefix"></div>
<div><label for="genSuffix">Suffix (e.g., Jr., M.D.):</label><input type="text" id="genSuffix" name="genSuffix"></div>
<div><label for="genNickname">Nickname:</label><input type="text" id="genNickname" name="genNickname"></div>
</fieldset>
<fieldset>
<legend>Personal Details</legend>
<div><label for="genGender">Gender:</label><input type="text" id="genGender" name="genGender" placeholder="M, F, O, U, N"></div>
<div><label for="genBday">Birthday (YYYY-MM-DD):</label><input type="text" id="genBday" name="genBday" placeholder="YYYY-MM-DD"></div>
<div><label for="genAnniversary">Anniversary (YYYY-MM-DD):</label><input type="text" id="genAnniversary" name="genAnniversary" placeholder="YYYY-MM-DD"></div>
</fieldset>
<fieldset>
<legend>Work/Organization</legend>
<div><label for="genOrg">Organization:</label><input type="text" id="genOrg" name="genOrg"></div>
<div><label for="genTitle">Title (Job Title):</label><input type="text" id="genTitle" name="genTitle"></div>
<div><label for="genRole">Role:</label><input type="text" id="genRole" name="genRole"></div>
</fieldset>
<fieldset id="phoneFieldsContainer">
<legend>Phone</legend>
<div class="phone-entry">
<div><label for="genPhone1">Phone Number:</label><input type="tel" id="genPhone1" name="genPhone1"></div>
<div><label for="genPhone1Type">Type:</label>
<select id="genPhone1Type" name="genPhone1Type">
<option value="CELL">Cell</option>
<option value="HOME,VOICE">Home</option>
<option value="WORK,VOICE">Work</option>
<option value="PAGER">Pager</option>
<option value="HOME,FAX">Home Fax</option>
<option value="WORK,FAX">Work Fax</option>
<option value="VOICE">Other Voice</option>
<option value="FAX">Other Fax</option>
</select>
</div>
</div>
<!-- Add button here later for more phones -->
</fieldset>
<fieldset id="emailFieldsContainer">
<legend>Email</legend>
<div class="email-entry">
<div><label for="genEmail1">Email Address:</label><input type="email" id="genEmail1" name="genEmail1"></div>
<div><label for="genEmail1Type">Type:</label>
<select id="genEmail1Type" name="genEmail1Type">
<option value="HOME,INTERNET">Home</option>
<option value="WORK,INTERNET">Work</option>
<option value="INTERNET">Other</option>
</select>
</div>
</div>
<!-- Add button here later for more emails -->
</fieldset>
<fieldset id="addressFieldsContainer">
<legend>Address</legend>
<div class="address-entry">
<div><label for="genAdr1Street">Street:</label><input type="text" id="genAdr1Street" name="genAdr1Street"></div>
<div><label for="genAdr1City">City:</label><input type="text" id="genAdr1City" name="genAdr1City"></div>
<div><label for="genAdr1State">State/Region:</label><input type="text" id="genAdr1State" name="genAdr1State"></div>
<div><label for="genAdr1Postal">Postal Code:</label><input type="text" id="genAdr1Postal" name="genAdr1Postal"></div>
<div><label for="genAdr1Country">Country:</label><input type="text" id="genAdr1Country" name="genAdr1Country"></div>
<div><label for="genAdr1Type">Type:</label>
<select id="genAdr1Type" name="genAdr1Type">
<option value="HOME">Home</option>
<option value="WORK">Work</option>
</select>
</div>
<div><label for="genAdr1Label">Label (Full Address):</label><textarea id="genAdr1Label" name="genAdr1Label" rows="3"></textarea></div>
</div>
<!-- Add button here later for more addresses -->
</fieldset>
<fieldset>
<legend>Online Presence</legend>
<div class="url-entry">
<div><label for="genUrl1">Website URL:</label><input type="url" id="genUrl1" name="genUrl1" placeholder="https://example.com"></div>
<div><label for="genUrl1Type">Type (e.g., WORK, HOME):</label><input type="text" id="genUrl1Type" name="genUrl1Type"></div>
</div>
<div class="socialprofile-entry">
<div><label for="genSocial1Type">Social Profile Type (e.g., facebook, twitter):</label><input type="text" id="genSocial1Type" name="genSocial1Type"></div>
<div><label for="genSocial1Value">Profile URL/Handle:</label><input type="text" id="genSocial1Value" name="genSocial1Value"></div>
</div>
</fieldset>
<fieldset>
<legend>Note</legend>
<div><label for="genNote">Note:</label><textarea id="genNote" name="genNote" rows="4"></textarea></div>
</fieldset>
<button type="button" id="generateVcfButton">Generate VCard</button>
</form>
<div id="generatedVCardArea" style="margin-top: 20px;">
<h3>Generated VCard:</h3>
<textarea id="vcfOutput" rows="15" style="width: 100%; font-family: monospace;" readonly></textarea>
<button type="button" id="downloadVcfButton" style="margin-top: 10px;">Download .vcf File</button>
</div>
</div> <!-- End of generatorView -->
<div id="privacyView" class="tab-pane">
<h1>Privacy Policy</h1>
<div class="privacy-content"> <!-- Using a wrapper for potentially more complex styling later -->
<p><strong>Effective Date:</strong> <span id="privacyEffectiveDate"></span></p>
<p>This VCard Utility (the "Service") is committed to respecting your privacy. This policy outlines how we handle information when you use our Service.</p>
<h2>1. Data Collection and Storage</h2>
<p>This Service operates entirely within your web browser on your local device.</p>
<ul>
<li><strong>No Data Storage:</strong> We do not store, save, or collect any of the VCard data you upload, input, or generate using this tool on our servers or any external database. All processing occurs client-side.</li>
<li><strong>No User Accounts:</strong> This Service does not require user registration or accounts.</li>
</ul>
<h2>2. Data Processing</h2>
<ul>
<li><strong>Client-Side Processing:</strong> When you upload a VCard for parsing or input data to generate a VCard, this information is processed directly in your browser. It is not transmitted to our servers.</li>
<li><strong>File Handling:</strong> Uploaded files are read by your browser for parsing. Generated VCards are created in your browser and can be downloaded directly to your device.</li>
</ul>
<h2>3. Cookies and Tracking Technologies</h2>
<ul>
<li><strong>No Cookies:</strong> This Service does not use cookies or any other tracking technologies to monitor your activity or collect personal information.</li>
</ul>
<h2>4. Your Responsibilities</h2>
<ul>
<li>You are responsible for the data you choose to upload to the Parser or input into the Generator. Please ensure you have the necessary rights and permissions for any personal data you handle using this tool.</li>
</ul>
<h2>5. GDPR (General Data Protection Regulation)</h2>
<ul>
<li>While this Service is designed not to collect or store personal data in a way that would typically make us a "data controller" or "data processor" under GDPR for the content of your VCards, we acknowledge the importance of data protection principles.</li>
<li>The processing of VCard data you provide is done at your instruction and under your control within your browser environment.</li>
</ul>
<h2>6. Changes to This Privacy Policy</h2>
<ul>
<li>We may update this Privacy Policy from time to time. Any changes will be posted on this page with an updated effective date. To update the date, modify the `privacyEffectiveDate` span or the script that populates it.</li>
</ul>
<h2>7. Contact Us</h2>
<ul>
<li>For questions about this policy, please note that this is a simple client-side tool with no backend data processing or storage provided by a service operator.</li>
</ul>
</div>
</div> <!-- End of privacyView -->
</div> <!-- End of main-container -->
</body> </body>
</html> </html>

361
script.js
View File

@ -39,6 +39,35 @@ function decodeQuotedPrintable(str, charset = 'UTF-8') {
} }
function formatPhoneNumber(phoneNumberString) {
if (!phoneNumberString || typeof phoneNumberString !== 'string' || phoneNumberString.trim() === 'N/A' || phoneNumberString.trim() === '') {
return phoneNumberString; // Return original if it's not a usable string
}
try {
// Attempt to parse the phone number.
// No default country is provided, so it relies on the number being in international format (e.g., +12125552368)
// or being a national number where the country can be easily inferred by the library (less reliable without a hint).
const phoneNumber = new libphonenumber.parsePhoneNumberFromString(phoneNumberString);
if (phoneNumber && phoneNumber.isValid()) {
return phoneNumber.formatInternational(); // e.g., +1 212 555 2368
} else {
// If it's not valid or couldn't be parsed well, try AsYouType for partial formatting
// This can sometimes make poorly formatted numbers a bit more readable.
const formattedAsYouType = new libphonenumber.AsYouType().input(phoneNumberString);
// AsYouType().input() might return an empty string if it processes everything and finds nothing valid,
// or it might return a partially formatted string. Only use if it's different and not empty.
if (formattedAsYouType && formattedAsYouType !== phoneNumberString) {
return formattedAsYouType;
}
return phoneNumberString; // Fallback to original if not valid or AsYouType didn't help
}
} catch (error) {
// console.error("Error formatting phone number:", phoneNumberString, error);
return phoneNumberString; // Fallback to original string in case of an error
}
}
function parseVCard(rawContent) { function parseVCard(rawContent) {
const contacts = []; const contacts = [];
// Handle line folding: join lines that start with a space or tab // Handle line folding: join lines that start with a space or tab
@ -176,9 +205,11 @@ function displayContactsInTable(contacts) {
// Mobile Phone // Mobile Phone
let mobilePhone = 'N/A'; let mobilePhone = 'N/A';
if (contact.tel && contact.tel.length > 0) { if (contact.tel && contact.tel.length > 0) {
const mobileEntry = contact.tel.find(t => t.params && t.params.TYPE && t.params.TYPE.toUpperCase().includes('CELL')); const mobileEntry = contact.tel.find(t =>
t.params && t.params.TYPE && Array.isArray(t.params.TYPE) && t.params.TYPE.includes('CELL')
);
if (mobileEntry) { if (mobileEntry) {
mobilePhone = mobileEntry.value; mobilePhone = formatPhoneNumber(mobileEntry.value);
} }
} }
row.insertCell().textContent = mobilePhone; row.insertCell().textContent = mobilePhone;
@ -207,7 +238,126 @@ function displayContactsInTable(contacts) {
}); });
} }
// --- Modal Logic --- // --- Tab Navigation Logic ---
const tabButtons = document.querySelectorAll('.tab-button');
const tabPanes = document.querySelectorAll('.tab-pane');
function showPane(paneIdToShow) {
tabPanes.forEach(pane => {
pane.classList.remove('active-pane');
// pane.style.display = 'none'; // Already handled by CSS .tab-pane
});
tabButtons.forEach(button => {
button.classList.remove('active-tab-button');
});
const paneToShow = document.getElementById(paneIdToShow);
if (paneToShow) {
paneToShow.classList.add('active-pane');
// paneToShow.style.display = 'block'; // Already handled by CSS .active-pane
}
// Find the button that corresponds to this paneId (e.g., 'parserView' -> 'parserTabButton')
const buttonToActivate = document.getElementById(paneIdToShow.replace('View', 'TabButton'));
if (buttonToActivate) {
buttonToActivate.classList.add('active-tab-button');
}
}
tabButtons.forEach(button => {
button.addEventListener('click', function() {
// Derive target pane ID from button ID e.g. "parserTabButton" -> "parserView"
const targetPaneId = this.id.replace('TabButton', 'View');
showPane(targetPaneId);
});
});
// Initialize with the Parser tab active and set up other event listeners
document.addEventListener('DOMContentLoaded', () => {
// Tab initialization
showPane('parserView');
// Set Privacy Policy Effective Date
const privacyEffectiveDateSpan = document.getElementById('privacyEffectiveDate');
if (privacyEffectiveDateSpan) {
const today = new Date();
const options = { year: 'numeric', month: 'long', day: 'numeric' };
privacyEffectiveDateSpan.textContent = today.toLocaleDateString(undefined, options); // Uses browser's default locale for formatting
}
// --- VCard Generator Logic ---
console.log("Attempting to find generator elements within DOMContentLoaded...");
const generateVcfButtonElem = document.getElementById('generateVcfButton');
const vcfOutputTextareaElem = document.getElementById('vcfOutput');
const downloadVcfButtonElem = document.getElementById('downloadVcfButton');
console.log("generateVcfButton found:", generateVcfButtonElem);
console.log("vcfOutputTextarea found:", vcfOutputTextareaElem);
console.log("downloadVcfButton found:", downloadVcfButtonElem);
// Element selection and event listener attachment for generator are inside DOMContentLoaded
// const generateVcfButton = document.getElementById('generateVcfButton'); // Replaced by Elem version
// const vcfOutputTextarea = document.getElementById('vcfOutput'); // Replaced by Elem version
// const downloadVcfButton = document.getElementById('downloadVcfButton'); // Replaced by Elem version
if (generateVcfButtonElem && vcfOutputTextareaElem) {
generateVcfButtonElem.addEventListener('click', function() { // Use Elem version
if (document.getElementById('vcardGeneratorForm').checkValidity()) {
const vcfString = generateVCardString();
vcfOutputTextareaElem.value = vcfString; // Use Elem version
} else {
alert("Please fill in all required fields (Formatted Name).");
document.getElementById('vcardGeneratorForm').reportValidity();
}
});
} else {
// These console errors will now only appear if the elements are truly missing from HTML
// or if this script somehow runs before even the generator tab's HTML is parsed (unlikely with DOMContentLoaded)
if (!generateVcfButtonElem) console.error("Generate VCF button ('generateVcfButton') not found in HTML.");
if (!vcfOutputTextareaElem && generateVcfButtonElem) console.error("VCF Output textarea ('vcfOutput') not found in HTML.");
}
if (downloadVcfButton && vcfOutputTextarea) {
downloadVcfButtonElem.addEventListener('click', function() { // Use Elem version
const vcfString = vcfOutputTextareaElem.value; // Use Elem version
if (!vcfString) {
alert("Please generate a VCard first.");
return;
}
let filename = "contact.vcf";
const fnValue = document.getElementById('genFN') ? document.getElementById('genFN').value.trim() : '';
if (fnValue) {
filename = fnValue.replace(/[^a-z0-9_ \.\-]/gi, '_') + ".vcf";
}
const blob = new Blob([vcfString], { type: 'text/vcard;charset=utf-8;' });
const link = document.createElement("a");
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} else {
alert("Your browser doesn't support direct download. Please copy the VCard content manually.");
}
});
} else {
// Corrected the condition here to check downloadVcfButtonElem
if (!downloadVcfButtonElem && generateVcfButtonElem) console.error("Download VCF button ('downloadVcfButton') not found in HTML.");
}
});
// --- Modal Logic (Parser's Modal) ---
// These are for the parser's modal and should be available once the initial HTML is parsed.
// They are not inside DOMContentLoaded because functions like displayContactsInTable (which sets up row listeners)
// might be called before DOMContentLoaded if a file is processed very quickly or if the script is deferred weirdly.
// However, standard practice is that these too would be safer inside, or their usage deferred.
// For now, this matches the structure that was working for the modal previously.
const modal = document.getElementById('contactModal'); const modal = document.getElementById('contactModal');
const closeButton = document.querySelector('.close-button'); const closeButton = document.querySelector('.close-button');
@ -221,10 +371,196 @@ if (modal && closeButton) {
} }
} }
} else { } else {
console.error("Modal or close button not found. Modal functionality will be affected."); // This error means the fundamental modal structure for the parser is missing.
console.error("Parser's Modal ('contactModal') or its close button not found. Modal functionality will be affected.");
} }
// --- VCard Generator Logic ---
const generateVcfButton = document.getElementById('generateVcfButton');
const vcfOutputTextarea = document.getElementById('vcfOutput');
function generateUUID() { // Basic UUID generator
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
function getCurrentTimestampUTC() {
const now = new Date();
return now.getUTCFullYear() +
('0' + (now.getUTCMonth() + 1)).slice(-2) +
('0' + now.getUTCDate()).slice(-2) + 'T' +
('0' + now.getUTCHours()).slice(-2) +
('0' + now.getUTCMinutes()).slice(-2) +
('0' + now.getUTCSeconds()).slice(-2) + 'Z';
}
function generateVCardString() {
const vcfData = [];
vcfData.push('BEGIN:VCARD');
vcfData.push('VERSION:3.0');
// Helper to add property if value exists
function addProperty(property, value, params = {}) {
if (value) {
let line = property;
if (Object.keys(params).length > 0) {
for (const key in params) {
if(params[key]) { // Ensure param value is not empty
line += `;${key}=${params[key]}`;
}
}
}
vcfData.push(`${line}:${value}`);
}
}
// Helper to get form value
function getFormValue(id) {
const element = document.getElementById(id);
return element ? element.value.trim() : '';
}
// Name
const fn = getFormValue('genFN');
addProperty('FN;CHARSET=UTF-8', fn);
const nValues = [
getFormValue('genFamilyName'),
getFormValue('genGivenName'),
getFormValue('genMiddleName'),
getFormValue('genPrefix'),
getFormValue('genSuffix')
];
if (nValues.some(v => v)) { // Only add N if at least one part exists
addProperty('N;CHARSET=UTF-8', nValues.join(';'));
}
addProperty('NICKNAME;CHARSET=UTF-8', getFormValue('genNickname'));
// Personal Details
addProperty('GENDER', getFormValue('genGender'));
const bday = getFormValue('genBday');
if (bday) addProperty('BDAY', bday.replace(/-/g, '')); // Convert YYYY-MM-DD to YYYYMMDD
const anniversary = getFormValue('genAnniversary');
if (anniversary) addProperty('ANNIVERSARY', anniversary.replace(/-/g, ''));
// Work/Organization
addProperty('ORG;CHARSET=UTF-8', getFormValue('genOrg'));
addProperty('TITLE;CHARSET=UTF-8', getFormValue('genTitle'));
addProperty('ROLE;CHARSET=UTF-8', getFormValue('genRole'));
// Phone 1
const phone1 = getFormValue('genPhone1');
const phone1Type = getFormValue('genPhone1Type');
if (phone1) {
addProperty(`TEL;TYPE=${phone1Type}`, phone1);
}
// Email 1
const email1 = getFormValue('genEmail1');
const email1Type = getFormValue('genEmail1Type');
if (email1) {
addProperty(`EMAIL;CHARSET=UTF-8;TYPE=${email1Type}`, email1);
}
// Address 1
const adr1Street = getFormValue('genAdr1Street');
const adr1City = getFormValue('genAdr1City');
const adr1State = getFormValue('genAdr1State');
const adr1Postal = getFormValue('genAdr1Postal');
const adr1Country = getFormValue('genAdr1Country');
const adr1Type = getFormValue('genAdr1Type');
const adr1Label = getFormValue('genAdr1Label');
if (adr1Street || adr1City || adr1State || adr1Postal || adr1Country) {
// ADR: PO Box; Extended Address; Street Address; Locality; Region; Postal Code; Country
const adrValue = `;;${adr1Street};${adr1City};${adr1State};${adr1Postal};${adr1Country}`;
addProperty(`ADR;CHARSET=UTF-8;TYPE=${adr1Type}`, adrValue);
}
if (adr1Label) {
addProperty(`LABEL;CHARSET=UTF-8;TYPE=${adr1Type}`, adr1Label);
}
// Online Presence
const url1 = getFormValue('genUrl1');
const url1Type = getFormValue('genUrl1Type');
if (url1) {
addProperty(url1Type ? `URL;TYPE=${url1Type.toUpperCase()}` : 'URL', url1);
}
const social1Type = getFormValue('genSocial1Type');
const social1Value = getFormValue('genSocial1Value');
if (social1Type && social1Value) {
addProperty(`X-SOCIALPROFILE;TYPE=${social1Type.toLowerCase()}`, social1Value);
}
// Note
addProperty('NOTE;CHARSET=UTF-8', getFormValue('genNote'));
// Auto-generated fields
addProperty('UID', generateUUID());
addProperty('REV', getCurrentTimestampUTC());
vcfData.push('END:VCARD');
return vcfData.join('\r\n'); // Standard VCF line ending
}
if (generateVcfButton) {
generateVcfButton.addEventListener('click', function() {
if (document.getElementById('vcardGeneratorForm').checkValidity()) {
const vcfString = generateVCardString();
if (vcfOutputTextarea) {
vcfOutputTextarea.value = vcfString;
}
} else {
// Optionally, provide more specific feedback or rely on browser's default validation UI
alert("Please fill in all required fields (Formatted Name).");
document.getElementById('vcardGeneratorForm').reportValidity();
}
});
} else {
console.error("Generate VCF button not found.");
}
const downloadVcfButton = document.getElementById('downloadVcfButton');
if (downloadVcfButton) {
downloadVcfButton.addEventListener('click', function() {
const vcfString = vcfOutputTextarea ? vcfOutputTextarea.value : '';
if (!vcfString) {
alert("Please generate a VCard first.");
return;
}
// Try to get a filename from FN, otherwise use a default
let filename = "contact.vcf";
const fnValue = document.getElementById('genFN') ? document.getElementById('genFN').value.trim() : '';
if (fnValue) {
filename = fnValue.replace(/[^a-z0-9_ \.\-]/gi, '_') + ".vcf"; // Sanitize filename
}
const blob = new Blob([vcfString], { type: 'text/vcard;charset=utf-8;' });
const link = document.createElement("a");
if (link.download !== undefined) { // feature detection
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} else {
alert("Your browser doesn't support direct download. Please copy the VCard content manually.");
}
});
} else {
console.error("Download VCF button not found.");
}
function constructFullName(contact) { function constructFullName(contact) {
if (contact.fn) { // Formatted Name usually preferred if available if (contact.fn) { // Formatted Name usually preferred if available
return contact.fn; return contact.fn;
@ -303,9 +639,16 @@ function populateModal(contact) {
if (contact.tel && contact.tel.length > 0) { if (contact.tel && contact.tel.length > 0) {
contact.tel.forEach(tel => { contact.tel.forEach(tel => {
const li = document.createElement('li'); const li = document.createElement('li');
let telDesc = tel.value; let formattedNumber = formatPhoneNumber(tel.value);
if (tel.params && tel.params.TYPE) { let telDesc = formattedNumber;
telDesc += ` (${tel.params.TYPE.split(',').join('/')})`;
if (tel.params && tel.params.TYPE && Array.isArray(tel.params.TYPE) && tel.params.TYPE.length > 0) {
// Add type information, but try not to duplicate if formatting already includes it (though unlikely for simple types)
const typeString = `(${tel.params.TYPE.join('/')})`;
// Avoid adding empty parenthesis if no types
if (typeString !== '()') {
telDesc += ` ${typeString}`;
}
} }
li.textContent = telDesc; li.textContent = telDesc;
phonesList.appendChild(li); phonesList.appendChild(li);
@ -321,8 +664,8 @@ function populateModal(contact) {
contact.email.forEach(email => { contact.email.forEach(email => {
const li = document.createElement('li'); const li = document.createElement('li');
let emailDesc = email.value; let emailDesc = email.value;
if (email.params && email.params.TYPE) { if (email.params && email.params.TYPE && Array.isArray(email.params.TYPE)) { // Ensure it's an array
emailDesc += ` (${email.params.TYPE.split(',').join('/')})`; emailDesc += ` (${email.params.TYPE.join('/')})`;
} }
li.textContent = emailDesc; li.textContent = emailDesc;
emailsList.appendChild(li); emailsList.appendChild(li);

402
style.css
View File

@ -1,14 +1,43 @@
body { body {
font-family: sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
margin: 20px; margin: 0; /* Remove default margin */
background-color: #f4f4f4; padding: 0; /* Ensure no default padding */
color: #333; background-color: #f8f9fa; /* Lighter grey background */
color: #212529; /* Darker text for better contrast */
line-height: 1.6;
font-size: 16px; /* Base font size */
}
h1, h2, h3 { /* Consistent heading styling */
color: #343a40; /* Dark grey for headings */
margin-top: 1.5em;
margin-bottom: 0.5em;
} }
h1 { h1 {
color: #333; font-size: 2.25rem; /* Larger H1 */
} }
h2 {
font-size: 1.75rem;
}
h3 {
font-size: 1.25rem;
}
.main-container {
max-width: 1140px; /* Common max-width for containers */
margin: 0 auto; /* Center the container */
padding: 20px;
background-color: #ffffff; /* White background for content area */
box-shadow: 0 0 10px rgba(0,0,0,0.05); /* Subtle shadow for depth */
border-radius: 8px; /* Slightly rounded corners for the container */
margin-top: 20px; /* Space from top of viewport */
margin-bottom: 20px; /* Space from bottom of viewport */
}
input[type="file"] { input[type="file"] {
margin-bottom: 20px; margin-bottom: 20px;
} }
@ -25,36 +54,43 @@ input[type="file"] {
#contactsTable { #contactsTable {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse; /* Keep collapsed borders */
margin-top: 20px; margin-top: 25px; /* Consistent margin */
table-layout: fixed; /* Helps with column width control */ table-layout: fixed;
font-size: 0.9rem; /* Slightly smaller font for table data */
background-color: #fff; /* Ensure table background is white if container has different color */
border: 1px solid #dee2e6; /* Outer border for the table */
border-radius: 0.25rem; /* Rounded corners for the table itself */
overflow: hidden; /* To make border-radius clip tbody/thead */
} }
#contactsTable th, #contactsTable td { #contactsTable th, #contactsTable td {
border: 1px solid #ddd; border: 1px solid #e9ecef; /* Lighter internal borders */
padding: 8px; padding: 12px 15px; /* Increased padding */
text-align: left; text-align: left;
word-wrap: break-word; /* Break long words to prevent overflow */ word-wrap: break-word;
} }
/* Style for the "Other Properties" column to ensure pre-wrap is effective */ /* Style for the "Other Properties" column to ensure pre-wrap is effective - No longer exists in main table */
#contactsTable td:last-child { /* #contactsTable td:last-child {
white-space: pre-wrap; white-space: pre-wrap;
} } */
#contactsTable th { #contactsTable th {
background-color: #e9e9e9; background-color: #f8f9fa; /* Very light grey for headers */
color: #333; color: #495057; /* Darker text for header contrast */
font-weight: 600; /* Bolder headers */
border-bottom: 2px solid #dee2e6; /* Stronger bottom border for headers */
} }
#contactsTable tr:nth-child(even) { #contactsTable tr:nth-child(even) td { /* Target td for striping background */
background-color: #f9f9f9; background-color: #f8f9fa; /* Light stripe for even rows */
} }
#contactsTable tr:hover { #contactsTable tr:hover td { /* Target td for hover background */
background-color: #f1f1f1; /* Highlight row on hover */ background-color: #e9ecef; /* Slightly darker hover for better feedback */
cursor: pointer; /* Indicate clickable */ cursor: pointer;
} }
@ -96,50 +132,316 @@ input[type="file"] {
color: black; color: black;
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
opacity: 1; /* From previous .close-button:hover, ensure it's here too */
} }
.modal h2, .modal h3 { /* Refined Modal Content Styles */
margin-top: 0; .modal-content h2 { /* Main modal title */
color: #333; font-size: 1.75rem; /* Was 1.75rem */
color: #007bff; /* Accent color for modal title */
margin-bottom: 20px; /* Was 0.5em, now more explicit */
padding-bottom: 10px;
border-bottom: 1px solid #eee; /* Keep this border */
margin-top: 0; /* Ensure no top margin for main title */
} }
.modal h3 { .modal-content h3 { /* Section titles within modal */
margin-top: 15px; font-size: 1.15rem; /* Was 1.25rem, slightly smaller for sub-sections */
border-bottom: 1px solid #eee; color: #343a40; /* Consistent with other h3s */
padding-bottom: 5px; margin-top: 25px; /* Increased space above section titles */
margin-bottom: 10px;
border-bottom: 1px dashed #ced4da; /* Dashed line for section separation */
padding-bottom: 8px;
} }
.modal ul { .modal-content p { /* Paragraphs in modal */
margin-bottom: 8px; /* Slightly more space */
line-height: 1.5; /* Improved readability */
}
.modal-content p strong { /* Bold text within paragraphs, e.g., "Family Name:" */
color: #495057; /* Softer, consistent color */
margin-right: 5px; /* Space after the label */
display: inline-block; /* Allows min-width if needed later */
/* min-width: 120px; */ /* Optional: if alignment is desired */
}
.modal-content ul {
list-style-type: none; list-style-type: none;
padding-left: 0; padding-left: 0;
margin-bottom: 15px; /* Space after lists */
} }
.modal ul li { .modal-content ul li {
background-color: #f9f9f9; background-color: #fff; /* Cleaner background for list items */
border: 1px solid #eee; border: 1px solid #e9ecef; /* Lighter border, consistent with table */
padding: 8px; padding: 10px 12px; /* Consistent padding */
margin-bottom: 5px; margin-bottom: 8px;
border-radius: 4px; border-radius: 0.25rem; /* Standard radius */
line-height: 1.5;
} }
.modal p { .modal-content ul li div { /* For nested divs within li, like formatted ADR */
margin-bottom: 5px; padding: 3px 0; /* Small padding for structure */
} }
.modal-content ul li div strong { /* For labels within formatted ADR, e.g., "Street:" */
.modal ul li div { /* For nested divs within li, like formatted ADR */
padding: 2px 0;
}
.modal ul li div strong { /* For labels within formatted ADR */
display: inline-block; display: inline-block;
min-width: 120px; /* Adjust as needed for alignment */ min-width: 100px; /* Adjust as needed */
margin-right: 5px; color: #495057;
margin-right: 8px;
font-weight: 500; /* Less strong than section headers */
} }
#modalNote { /* Ensure note paragraph respects newlines from VCard */ #modalNote {
white-space: pre-wrap; white-space: pre-wrap;
background-color: #f9f9f9; background-color: #f8f9fa; /* Consistent light background */
border: 1px solid #eee; border: 1px solid #dee2e6; /* Consistent border */
padding: 8px; padding: 12px;
border-radius: 4px; border-radius: 0.25rem;
min-height: 50px; /* Give it some space */ min-height: 60px;
line-height: 1.5;
color: #212529; /* Ensure text color is set */
font-size: 0.95rem; /* Slightly smaller for notes */
}
/* Tab Navigation Styles */
.tab-navigation {
border-bottom: 2px solid #dee2e6; /* Slightly thicker border */
margin-bottom: 25px; /* Increased margin */
display: flex;
}
.tab-button {
background-color: transparent; /* Make inactive tabs blend more */
border: none; /* Remove default border */
border-bottom: 2px solid transparent; /* Placeholder for active state */
padding: 12px 18px; /* Adjusted padding */
cursor: pointer;
font-size: 1rem; /* Use rem for font size */
margin-right: 8px;
border-radius: 0; /* Sharp corners for a more modern tab look */
outline: none;
transition: color 0.2s ease-in-out, border-color 0.2s ease-in-out;
color: #495057; /* Softer text color for inactive tabs */
}
.tab-button:hover {
color: #007bff; /* Accent color on hover */
border-bottom-color: #cfe2ff; /* Light blue bottom border on hover */
}
.tab-button.active-tab-button {
color: #007bff; /* Active tab text color */
border-bottom-color: #007bff; /* Active tab underline */
font-weight: 600; /* Slightly bolder for active tab */
}
/* Tab Pane Styles */
.tab-pane {
display: none; /* Hidden by default */
padding: 10px;
/* border: 1px solid #ccc; */ /* Optional: border around content */
/* border-top: none; */ /* Optional: if tab-navigation has bottom border */
}
.tab-pane.active-pane {
display: block; /* Shown when active */
}
.tab-description {
background-color: #e9ecef; /* Light grey background, distinct from main content area */
padding: 15px;
margin-top: 0; /* Align with top if h1 has margin-top */
margin-bottom: 25px; /* Space before the main content of the tab */
border-radius: 0.25rem; /* Consistent border-radius */
font-size: 0.95rem; /* Slightly smaller font for descriptive text */
line-height: 1.6;
color: #495057; /* Softer text color */
}
.tab-description p {
margin-top: 0;
margin-bottom: 10px; /* Space between paragraphs within the description */
}
.tab-description p:last-child {
margin-bottom: 0; /* No bottom margin for the last paragraph */
}
/* VCard Generator Form Styles */
#vcardGeneratorForm fieldset {
margin-bottom: 20px;
border: 1px solid #ddd;
padding: 15px;
border-radius: 8px; /* Consistent with main-container */
background-color: #fdfdfd; /* Slightly off-white for fieldset */
}
#vcardGeneratorForm legend {
font-weight: 600; /* Bolder legend */
color: #007bff; /* Accent color for legend */
padding: 0 10px; /* More padding around legend text */
font-size: 1.1rem;
}
#vcardGeneratorForm div { /* Container for label + input */
margin-bottom: 15px; /* Increased spacing */
}
#vcardGeneratorForm label {
display: block;
margin-bottom: 5px; /* Increased space below label */
font-weight: 500;
color: #495057; /* Slightly softer label color */
font-size: 0.95rem;
}
#vcardGeneratorForm input[type="text"],
#vcardGeneratorForm input[type="tel"],
#vcardGeneratorForm input[type="email"],
#vcardGeneratorForm input[type="url"],
#vcardGeneratorForm select,
#vcardGeneratorForm textarea {
width: 100%; /* Let box-sizing handle padding/border */
padding: 10px 12px; /* Increased padding */
border: 1px solid #ced4da; /* Standard Bootstrap-like border color */
border-radius: 0.25rem; /* Standard border-radius */
box-sizing: border-box;
font-size: 1rem; /* Consistent font size */
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
#vcardGeneratorForm input[type="text"]:focus,
#vcardGeneratorForm input[type="tel"]:focus,
#vcardGeneratorForm input[type="email"]:focus,
#vcardGeneratorForm input[type="url"]:focus,
#vcardGeneratorForm select:focus,
#vcardGeneratorForm textarea:focus {
border-color: #80bdff; /* Bootstrap focus color */
outline: 0;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); /* Bootstrap focus shadow */
}
#vcardGeneratorForm textarea {
resize: vertical;
min-height: 80px; /* Minimum height for textareas */
}
#vcardGeneratorForm button[type="button"] {
background-color: #007bff;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
#vcardGeneratorForm button[type="button"]:hover {
transition: background-color 0.3s ease-in-out, border-color 0.3s ease-in-out;
/* General button styling - primary actions */
}
#vcardGeneratorForm button[type="button"]:hover {
background-color: #0056b3;
border-color: #0056b3;
}
/* Download button specific styling (can override general if needed, or be a class) */
#downloadVcfButton {
background-color: #28a745;
border-color: #28a745;
}
#downloadVcfButton:hover {
background-color: #1e7e34;
border-color: #1e7e34;
}
/* File Input Styling - common trick using a label */
input[type="file"] {
display: none; /* Hide the actual file input */
}
.file-upload-label {
display: inline-block;
background-color: #6c757d; /* Secondary button color */
color: white;
padding: 10px 15px;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.2s ease-in-out;
margin-bottom: 20px; /* Keep existing margin */
}
.file-upload-label:hover {
background-color: #5a6268;
}
/* Modal Close Button - already has some styling, let's refine slightly */
.close-button {
color: #6c757d; /* Softer color */
float: right;
font-size: 1.75rem; /* Slightly larger */
font-weight: bold;
line-height: 1; /* Ensure it aligns well */
opacity: 0.75;
transition: color 0.15s ease-in-out, opacity 0.15s ease-in-out;
}
.close-button:hover,
.close-button:focus {
color: #343a40; /* Darker on hover/focus */
text-decoration: none;
cursor: pointer;
opacity: 1;
}
/* Specific layout for grouped inline elements if any (e.g. phone + type) */
/* For now, using display:block on labels makes them stack nicely */
/* Style for the generated VCard display area */
#generatedVCardArea h3 {
margin-top: 10px; /* Add some space above this heading */
}
/* Privacy Policy Tab Specific Styles */
#privacyView .privacy-content {
padding: 10px 0; /* Padding for top/bottom, horizontal handled by .main-container or .tab-pane */
line-height: 1.7;
font-size: 0.95rem;
color: #343a40; /* Slightly darker than tab-description for more formal text */
}
#privacyView .privacy-content h1 { /* Already styled globally, but ensure it's distinct if needed */
margin-bottom: 25px; /* More space after main Privacy Policy title */
}
#privacyView .privacy-content h2 {
font-size: 1.3rem;
color: #007bff;
margin-top: 30px; /* More space before each section */
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 1px solid #e0e0e0;
}
#privacyView .privacy-content p {
margin-bottom: 15px; /* Space between paragraphs */
}
#privacyView .privacy-content ul {
list-style-type: disc;
padding-left: 25px; /* Indent list items further */
margin-bottom: 20px; /* Space after lists */
}
#privacyView .privacy-content li {
margin-bottom: 10px; /* Space between list items */
} }