fix(resume): separate JS file for resume logic
- resume-builder.js in public folder - cleaner React component - script loaded from public
This commit is contained in:
@@ -5,18 +5,13 @@ import BackToMC from "@/components/mission-control/BackToMC";
|
|||||||
|
|
||||||
export default function ResumeBuilderPage() {
|
export default function ResumeBuilderPage() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Load the resume script after component mounts
|
// Script is loaded from public/resume-builder.js
|
||||||
const script = document.createElement("script");
|
|
||||||
script.src = "/resume-builder.js";
|
|
||||||
script.async = true;
|
|
||||||
document.body.appendChild(script);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-200">
|
<div className="min-h-screen bg-slate-200">
|
||||||
<BackToMC />
|
<BackToMC />
|
||||||
|
|
||||||
{/* Include the styles inline */}
|
|
||||||
<style>{`
|
<style>{`
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap');
|
||||||
|
|
||||||
@@ -56,6 +51,7 @@ export default function ResumeBuilderPage() {
|
|||||||
margin: 0 auto 2rem;
|
margin: 0 auto 2rem;
|
||||||
background: #d1d1d1;
|
background: #d1d1d1;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.circle-frame img {
|
.circle-frame img {
|
||||||
@@ -73,7 +69,6 @@ export default function ResumeBuilderPage() {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
cursor: pointer;
|
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -104,18 +99,18 @@ export default function ResumeBuilderPage() {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
[contenteditable="true"] {
|
[contentEditable="true"] {
|
||||||
outline: none;
|
outline: none;
|
||||||
border: 1px dashed transparent;
|
border: 1px dashed transparent;
|
||||||
transition: border 0.2s;
|
transition: border 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
[contenteditable="true"]:hover {
|
[contentEditable="true"]:hover {
|
||||||
border: 1px dashed #3b82f6;
|
border: 1px dashed #3b82f6;
|
||||||
background-color: rgba(59, 130, 246, 0.05);
|
background-color: rgba(59, 130, 246, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
[contenteditable="true"]:focus {
|
[contentEditable="true"]:focus {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border: 1px solid #3b82f6;
|
border: 1px solid #3b82f6;
|
||||||
}
|
}
|
||||||
@@ -150,16 +145,14 @@ export default function ResumeBuilderPage() {
|
|||||||
.no-print, .toolbar, .photo-edit-overlay { display: none !important; }
|
.no-print, .toolbar, .photo-edit-overlay { display: none !important; }
|
||||||
body { background: white; padding: 0; margin: 0; }
|
body { background: white; padding: 0; margin: 0; }
|
||||||
.resume-page { margin: 0; box-shadow: none; width: 100%; }
|
.resume-page { margin: 0; box-shadow: none; width: 100%; }
|
||||||
[contenteditable="true"]:hover { border-color: transparent; background: transparent; }
|
[contentEditable="true"]:hover { border-color: transparent; background: transparent; }
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
{/* Toolbar */}
|
|
||||||
<div className="sticky top-0 z-50 bg-gray-800 px-6 py-3 flex items-center justify-between">
|
<div className="sticky top-0 z-50 bg-gray-800 px-6 py-3 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<h1 className="font-bold text-lg text-blue-400">Haitham's Resume Manager</h1>
|
<h1 className="font-bold text-lg text-blue-400">Haitham's Resume Manager</h1>
|
||||||
<select id="resumeSelect" className="bg-gray-700 text-white px-3 py-1 rounded border border-gray-600 outline-none">
|
<select id="resumeSelect" className="bg-gray-700 text-white px-3 py-1 rounded border border-gray-600 outline-none" />
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button id="btnNew" className="bg-blue-600 hover:bg-blue-700 px-4 py-1 rounded text-sm font-medium transition">New Version</button>
|
<button id="btnNew" className="bg-blue-600 hover:bg-blue-700 px-4 py-1 rounded text-sm font-medium transition">New Version</button>
|
||||||
@@ -169,9 +162,7 @@ export default function ResumeBuilderPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Resume Container */}
|
|
||||||
<div className="resume-page mt-20">
|
<div className="resume-page mt-20">
|
||||||
{/* Sidebar */}
|
|
||||||
<div className="sidebar">
|
<div className="sidebar">
|
||||||
<div className="circle-frame" id="photoFrame">
|
<div className="circle-frame" id="photoFrame">
|
||||||
<img id="userPhoto" src="Resume.jpg" alt="Haitham Khalifa" />
|
<img id="userPhoto" src="Resume.jpg" alt="Haitham Khalifa" />
|
||||||
@@ -181,7 +172,6 @@ export default function ResumeBuilderPage() {
|
|||||||
|
|
||||||
<h1 id="userName" contentEditable suppressContentEditableWarning className="text-2xl font-black text-center text-gray-900 mb-8 tracking-tighter uppercase leading-none">HAITHAM KHALIFA</h1>
|
<h1 id="userName" contentEditable suppressContentEditableWarning className="text-2xl font-black text-center text-gray-900 mb-8 tracking-tighter uppercase leading-none">HAITHAM KHALIFA</h1>
|
||||||
|
|
||||||
{/* Contact */}
|
|
||||||
<div className="space-y-3 text-[11px] text-gray-700 mb-10">
|
<div className="space-y-3 text-[11px] text-gray-700 mb-10">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="w-5 h-5 flex items-center justify-center bg-gray-800 text-white rounded-full text-[10px]">📞</span>
|
<span className="w-5 h-5 flex items-center justify-center bg-gray-800 text-white rounded-full text-[10px]">📞</span>
|
||||||
@@ -245,12 +235,10 @@ export default function ResumeBuilderPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Experience */}
|
|
||||||
<div className="main-content">
|
<div className="main-content">
|
||||||
<h2 className="main-header">Experience</h2>
|
<h2 className="main-header">Experience</h2>
|
||||||
<div id="experienceContainer" contentEditable suppressContentEditableWarning className="space-y-10">
|
<div id="experienceContainer" contentEditable suppressContentEditableWarning className="space-y-10">
|
||||||
|
|
||||||
{/* Protein.com */}
|
|
||||||
<div className="relative pl-8 border-l-2 border-gray-200 pb-2">
|
<div className="relative pl-8 border-l-2 border-gray-200 pb-2">
|
||||||
<div className="experience-dot"></div>
|
<div className="experience-dot"></div>
|
||||||
<div className="flex justify-between items-baseline mb-1">
|
<div className="flex justify-between items-baseline mb-1">
|
||||||
@@ -268,7 +256,6 @@ export default function ResumeBuilderPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* HostPioneers */}
|
|
||||||
<div className="relative pl-8 border-l-2 border-gray-200 pb-2">
|
<div className="relative pl-8 border-l-2 border-gray-200 pb-2">
|
||||||
<div className="experience-dot experience-dot-sub"></div>
|
<div className="experience-dot experience-dot-sub"></div>
|
||||||
<div className="flex justify-between items-baseline mb-1">
|
<div className="flex justify-between items-baseline mb-1">
|
||||||
@@ -286,7 +273,6 @@ export default function ResumeBuilderPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Red Cross */}
|
|
||||||
<div className="relative pl-8 border-l-2 border-gray-200 pb-2">
|
<div className="relative pl-8 border-l-2 border-gray-200 pb-2">
|
||||||
<div className="experience-dot experience-dot-sub"></div>
|
<div className="experience-dot experience-dot-sub"></div>
|
||||||
<div className="flex justify-between items-baseline mb-1">
|
<div className="flex justify-between items-baseline mb-1">
|
||||||
@@ -301,7 +287,6 @@ export default function ResumeBuilderPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cayenne */}
|
|
||||||
<div className="relative pl-8 border-l-2 border-gray-200 pb-2">
|
<div className="relative pl-8 border-l-2 border-gray-200 pb-2">
|
||||||
<div className="experience-dot experience-dot-sub"></div>
|
<div className="experience-dot experience-dot-sub"></div>
|
||||||
<div className="flex justify-between items-baseline mb-1">
|
<div className="flex justify-between items-baseline mb-1">
|
||||||
@@ -321,137 +306,7 @@ export default function ResumeBuilderPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* JavaScript */}
|
<script src="/resume-builder.js"></script>
|
||||||
<script dangerouslySetInnerHTML={{ __html: `
|
|
||||||
let resumes = JSON.parse(localStorage.getItem('haitham_resumes_v2') || '{}');
|
|
||||||
let currentProfileName = localStorage.getItem('haitham_current_profile_v2') || 'Main Resume';
|
|
||||||
|
|
||||||
const getEl = (id) => document.getElementById(id);
|
|
||||||
|
|
||||||
const getInitialContent = () => ({
|
|
||||||
name: getEl('userName')?.innerText || '',
|
|
||||||
photo: getEl('userPhoto')?.src || '',
|
|
||||||
phone: getEl('userPhone')?.innerText || '',
|
|
||||||
email: getEl('userEmail')?.innerText || '',
|
|
||||||
linkedin: getEl('userLinkedin')?.innerText || '',
|
|
||||||
location: getEl('userLocation')?.innerText || '',
|
|
||||||
about: getEl('aboutMe')?.innerText || '',
|
|
||||||
education: getEl('educationList')?.innerHTML || '',
|
|
||||||
skills: getEl('skillsList')?.innerHTML || '',
|
|
||||||
language: getEl('languageList')?.innerHTML || '',
|
|
||||||
experience: getEl('experienceContainer')?.innerHTML || ''
|
|
||||||
});
|
|
||||||
|
|
||||||
const init = () => {
|
|
||||||
if (Object.keys(resumes).length === 0) {
|
|
||||||
resumes['Main Resume'] = getInitialContent();
|
|
||||||
saveToDisk();
|
|
||||||
}
|
|
||||||
updateSelect();
|
|
||||||
loadProfile(currentProfileName);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePhotoUpload = (input) => {
|
|
||||||
const file = input.files[0];
|
|
||||||
if (file) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
getEl('userPhoto').src = e.target.result;
|
|
||||||
saveCurrentProfile();
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
getEl('photoInput')?.addEventListener('change', (e) => handlePhotoUpload(e.target));
|
|
||||||
getEl('photoFrame')?.addEventListener('click', () => getEl('photoInput')?.click());
|
|
||||||
|
|
||||||
const updateSelect = () => {
|
|
||||||
const select = getEl('resumeSelect');
|
|
||||||
if (!select) return;
|
|
||||||
select.innerHTML = '';
|
|
||||||
Object.keys(resumes).forEach(name => {
|
|
||||||
const opt = document.createElement('option');
|
|
||||||
opt.value = name;
|
|
||||||
opt.textContent = name;
|
|
||||||
if (name === currentProfileName) opt.selected = true;
|
|
||||||
select.appendChild(opt);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadProfile = (name) => {
|
|
||||||
const p = resumes[name] || resumes[Object.keys(resumes)[0]];
|
|
||||||
if (!p) return;
|
|
||||||
currentProfileName = name;
|
|
||||||
localStorage.setItem('haitham_current_profile_v2', name);
|
|
||||||
|
|
||||||
getEl('userName').innerText = p.name || '';
|
|
||||||
getEl('userPhoto').src = p.photo || 'Resume.jpg';
|
|
||||||
getEl('userPhone').innerText = p.phone || '';
|
|
||||||
getEl('userEmail').innerText = p.email || '';
|
|
||||||
getEl('userLinkedin').innerText = p.linkedin || '';
|
|
||||||
getEl('userLocation').innerText = p.location || '';
|
|
||||||
getEl('aboutMe').innerText = p.about || '';
|
|
||||||
getEl('educationList').innerHTML = p.education || '';
|
|
||||||
getEl('skillsList').innerHTML = p.skills || '';
|
|
||||||
getEl('languageList').innerHTML = p.language || '';
|
|
||||||
getEl('experienceContainer').innerHTML = p.experience || '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveCurrentProfile = () => {
|
|
||||||
resumes[currentProfileName] = getInitialContent();
|
|
||||||
saveToDisk();
|
|
||||||
showStatus('Changes Saved');
|
|
||||||
};
|
|
||||||
|
|
||||||
const createNewProfile = () => {
|
|
||||||
const name = prompt('Name this version (e.g., Geely IT Manager):');
|
|
||||||
if (name && !resumes[name]) {
|
|
||||||
resumes[name] = { ...resumes[currentProfileName] };
|
|
||||||
currentProfileName = name;
|
|
||||||
saveToDisk();
|
|
||||||
updateSelect();
|
|
||||||
loadProfile(name);
|
|
||||||
} else if (resumes[name]) {
|
|
||||||
alert('This name already exists.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteProfile = () => {
|
|
||||||
if (Object.keys(resumes).length <= 1) return alert('You need at least one profile.');
|
|
||||||
if (confirm('Delete "' + currentProfileName + '"?')) {
|
|
||||||
delete resumes[currentProfileName];
|
|
||||||
currentProfileName = Object.keys(resumes)[0];
|
|
||||||
saveToDisk();
|
|
||||||
updateSelect();
|
|
||||||
loadProfile(currentProfileName);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveToDisk = () => {
|
|
||||||
localStorage.setItem('haitham_resumes_v2', JSON.stringify(resumes));
|
|
||||||
};
|
|
||||||
|
|
||||||
const showStatus = (msg) => {
|
|
||||||
const btn = getEl('btnSave');
|
|
||||||
if (!btn) return;
|
|
||||||
const original = btn.textContent;
|
|
||||||
btn.textContent = msg;
|
|
||||||
btn.style.backgroundColor = '#059669';
|
|
||||||
setTimeout(() => {
|
|
||||||
btn.textContent = original;
|
|
||||||
btn.style.backgroundColor = '';
|
|
||||||
}, 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
getEl('btnNew')?.addEventListener('click', createNewProfile);
|
|
||||||
getEl('btnSave')?.addEventListener('click', saveCurrentProfile);
|
|
||||||
getEl('btnDelete')?.addEventListener('click', deleteProfile);
|
|
||||||
getEl('btnPrint')?.addEventListener('click', () => window.print());
|
|
||||||
getEl('resumeSelect')?.addEventListener('change', (e) => loadProfile(e.target.value));
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
|
||||||
`}} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
// Resume Builder JavaScript
|
||||||
|
let resumes = JSON.parse(localStorage.getItem('haitham_resumes_v2') || '{}');
|
||||||
|
let currentProfileName = localStorage.getItem('haitham_current_profile_v2') || 'Main Resume';
|
||||||
|
|
||||||
|
const getEl = (id) => document.getElementById(id);
|
||||||
|
|
||||||
|
const getInitialContent = () => ({
|
||||||
|
name: getEl('userName')?.innerText || '',
|
||||||
|
photo: getEl('userPhoto')?.src || '',
|
||||||
|
phone: getEl('userPhone')?.innerText || '',
|
||||||
|
email: getEl('userEmail')?.innerText || '',
|
||||||
|
linkedin: getEl('userLinkedin')?.innerText || '',
|
||||||
|
location: getEl('userLocation')?.innerText || '',
|
||||||
|
about: getEl('aboutMe')?.innerText || '',
|
||||||
|
education: getEl('educationList')?.innerHTML || '',
|
||||||
|
skills: getEl('skillsList')?.innerHTML || '',
|
||||||
|
language: getEl('languageList')?.innerHTML || '',
|
||||||
|
experience: getEl('experienceContainer')?.innerHTML || ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
if (Object.keys(resumes).length === 0) {
|
||||||
|
resumes['Main Resume'] = getInitialContent();
|
||||||
|
saveToDisk();
|
||||||
|
}
|
||||||
|
updateSelect();
|
||||||
|
loadProfile(currentProfileName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePhotoUpload = (input) => {
|
||||||
|
const file = input.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
getEl('userPhoto').src = e.target.result;
|
||||||
|
saveCurrentProfile();
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const photoInput = getEl('photoInput');
|
||||||
|
const photoFrame = getEl('photoFrame');
|
||||||
|
if (photoInput && photoFrame) {
|
||||||
|
photoInput.addEventListener('change', (e) => handlePhotoUpload(e.target));
|
||||||
|
photoFrame.addEventListener('click', () => photoInput.click());
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSelect = () => {
|
||||||
|
const select = getEl('resumeSelect');
|
||||||
|
if (!select) return;
|
||||||
|
select.innerHTML = '';
|
||||||
|
Object.keys(resumes).forEach(name => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = name;
|
||||||
|
opt.textContent = name;
|
||||||
|
if (name === currentProfileName) opt.selected = true;
|
||||||
|
select.appendChild(opt);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadProfile = (name) => {
|
||||||
|
const p = resumes[name] || resumes[Object.keys(resumes)[0]];
|
||||||
|
if (!p) return;
|
||||||
|
currentProfileName = name;
|
||||||
|
localStorage.setItem('haitham_current_profile_v2', name);
|
||||||
|
|
||||||
|
getEl('userName').innerText = p.name || '';
|
||||||
|
getEl('userPhoto').src = p.photo || 'Resume.jpg';
|
||||||
|
getEl('userPhone').innerText = p.phone || '';
|
||||||
|
getEl('userEmail').innerText = p.email || '';
|
||||||
|
getEl('userLinkedin').innerText = p.linkedin || '';
|
||||||
|
getEl('userLocation').innerText = p.location || '';
|
||||||
|
getEl('aboutMe').innerText = p.about || '';
|
||||||
|
getEl('educationList').innerHTML = p.education || '';
|
||||||
|
getEl('skillsList').innerHTML = p.skills || '';
|
||||||
|
getEl('languageList').innerHTML = p.language || '';
|
||||||
|
getEl('experienceContainer').innerHTML = p.experience || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveCurrentProfile = () => {
|
||||||
|
resumes[currentProfileName] = getInitialContent();
|
||||||
|
saveToDisk();
|
||||||
|
showStatus('Changes Saved');
|
||||||
|
};
|
||||||
|
|
||||||
|
const createNewProfile = () => {
|
||||||
|
const name = prompt('Name this version (e.g., Geely IT Manager):');
|
||||||
|
if (name && !resumes[name]) {
|
||||||
|
resumes[name] = { ...resumes[currentProfileName] };
|
||||||
|
currentProfileName = name;
|
||||||
|
saveToDisk();
|
||||||
|
updateSelect();
|
||||||
|
loadProfile(name);
|
||||||
|
} else if (resumes[name]) {
|
||||||
|
alert('This name already exists.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteProfile = () => {
|
||||||
|
if (Object.keys(resumes).length <= 1) return alert('You need at least one profile.');
|
||||||
|
if (confirm('Delete "' + currentProfileName + '"?')) {
|
||||||
|
delete resumes[currentProfileName];
|
||||||
|
currentProfileName = Object.keys(resumes)[0];
|
||||||
|
saveToDisk();
|
||||||
|
updateSelect();
|
||||||
|
loadProfile(currentProfileName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveToDisk = () => {
|
||||||
|
localStorage.setItem('haitham_resumes_v2', JSON.stringify(resumes));
|
||||||
|
};
|
||||||
|
|
||||||
|
const showStatus = (msg) => {
|
||||||
|
const btn = getEl('btnSave');
|
||||||
|
if (!btn) return;
|
||||||
|
const original = btn.textContent;
|
||||||
|
btn.textContent = msg;
|
||||||
|
btn.style.backgroundColor = '#059669';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = original;
|
||||||
|
btn.style.backgroundColor = '';
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const btnNew = getEl('btnNew');
|
||||||
|
const btnSave = getEl('btnSave');
|
||||||
|
const btnDelete = getEl('btnDelete');
|
||||||
|
const btnPrint = getEl('btnPrint');
|
||||||
|
const resumeSelect = getEl('resumeSelect');
|
||||||
|
|
||||||
|
if (btnNew) btnNew.addEventListener('click', createNewProfile);
|
||||||
|
if (btnSave) btnSave.addEventListener('click', saveCurrentProfile);
|
||||||
|
if (btnDelete) btnDelete.addEventListener('click', deleteProfile);
|
||||||
|
if (btnPrint) btnPrint.addEventListener('click', () => window.print());
|
||||||
|
if (resumeSelect) resumeSelect.addEventListener('change', (e) => loadProfile(e.target.value));
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
Reference in New Issue
Block a user