update
This commit is contained in:
+197
-34
@@ -50,24 +50,26 @@ function initializeDarkMode() {
|
|||||||
// Check localStorage for dark mode preference
|
// Check localStorage for dark mode preference
|
||||||
const darkMode = localStorage.getItem('darkMode');
|
const darkMode = localStorage.getItem('darkMode');
|
||||||
|
|
||||||
|
// The `dark` class must live on <html> so that Tailwind's `.dark .dark:*`
|
||||||
|
// selectors apply to the <body> itself, not just its descendants.
|
||||||
if (darkMode === 'enabled' || (!darkMode && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
if (darkMode === 'enabled' || (!darkMode && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
body.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
sunIcon.classList.add('hidden');
|
sunIcon.classList.add('hidden');
|
||||||
moonIcon.classList.remove('hidden');
|
moonIcon.classList.remove('hidden');
|
||||||
} else {
|
} else {
|
||||||
body.classList.remove('dark');
|
document.documentElement.classList.remove('dark');
|
||||||
sunIcon.classList.remove('hidden');
|
sunIcon.classList.remove('hidden');
|
||||||
moonIcon.classList.add('hidden');
|
moonIcon.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleDarkMode() {
|
function toggleDarkMode() {
|
||||||
body.classList.toggle('dark');
|
document.documentElement.classList.toggle('dark');
|
||||||
sunIcon.classList.toggle('hidden');
|
sunIcon.classList.toggle('hidden');
|
||||||
moonIcon.classList.toggle('hidden');
|
moonIcon.classList.toggle('hidden');
|
||||||
|
|
||||||
// Save preference to localStorage
|
// Save preference to localStorage
|
||||||
if (body.classList.contains('dark')) {
|
if (document.documentElement.classList.contains('dark')) {
|
||||||
localStorage.setItem('darkMode', 'enabled');
|
localStorage.setItem('darkMode', 'enabled');
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem('darkMode', 'disabled');
|
localStorage.setItem('darkMode', 'disabled');
|
||||||
@@ -275,7 +277,8 @@ function generatePDF(event) {
|
|||||||
|
|
||||||
// PDF Generation with jsPDF
|
// PDF Generation with jsPDF
|
||||||
function generatePdfDocument(settings, applications, month, year) {
|
function generatePdfDocument(settings, applications, month, year) {
|
||||||
// Check if jsPDF is loaded
|
// The UMD build exposes the constructor as window.jspdf.jsPDF, not as a global jsPDF
|
||||||
|
const jsPDF = window.jspdf && window.jspdf.jsPDF;
|
||||||
if (typeof jsPDF === 'undefined') {
|
if (typeof jsPDF === 'undefined') {
|
||||||
console.error('jsPDF not loaded, please wait for libraries to load');
|
console.error('jsPDF not loaded, please wait for libraries to load');
|
||||||
alert('Bitte warten Sie einen Moment und versuchen Sie es erneut.');
|
alert('Bitte warten Sie einen Moment und versuchen Sie es erneut.');
|
||||||
@@ -292,7 +295,18 @@ function generatePdfDocument(settings, applications, month, year) {
|
|||||||
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
|
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
|
||||||
|
|
||||||
const monthName = month ? monthNames[parseInt(month) - 1] : 'alle';
|
const monthName = month ? monthNames[parseInt(month) - 1] : 'alle';
|
||||||
const title = `Bewerbungsaktivitäten - ${monthName} ${year || 'Jahre'}`;
|
// Determine the year shown in the title. When no year was picked in the form,
|
||||||
|
// derive it from the data so the title still reads e.g. "Juni 2026".
|
||||||
|
const yearsInData = [...new Set(
|
||||||
|
applications.map((a) => new Date(a.datum).getFullYear()).filter((y) => !isNaN(y))
|
||||||
|
)].sort((a, b) => a - b);
|
||||||
|
const displayYear = year
|
||||||
|
|| (yearsInData.length === 1 ? String(yearsInData[0])
|
||||||
|
: yearsInData.length > 1 ? `${yearsInData[0]}–${yearsInData[yearsInData.length - 1]}` : '');
|
||||||
|
const period = month
|
||||||
|
? `${monthName}${displayYear ? ' ' + displayYear : ''}`
|
||||||
|
: (displayYear || 'alle Zeiträume');
|
||||||
|
const title = `Bewerbungsaktivitäten - ${period}`;
|
||||||
const dateStr = new Date().toLocaleDateString('de-DE');
|
const dateStr = new Date().toLocaleDateString('de-DE');
|
||||||
|
|
||||||
// Page geometry
|
// Page geometry
|
||||||
@@ -318,38 +332,42 @@ function generatePdfDocument(settings, applications, month, year) {
|
|||||||
doc.text(title, pageWidth / 2, yPos, { align: 'center' });
|
doc.text(title, pageWidth / 2, yPos, { align: 'center' });
|
||||||
yPos += 12;
|
yPos += 12;
|
||||||
|
|
||||||
// User information
|
// User information. Values like the address can span multiple lines, so we
|
||||||
|
// split them and advance yPos by the actual number of rendered lines –
|
||||||
|
// otherwise the next field overlaps the wrapped text.
|
||||||
doc.setFontSize(12);
|
doc.setFontSize(12);
|
||||||
doc.setFont('helvetica', 'normal');
|
doc.setFont('helvetica', 'normal');
|
||||||
if (settings && settings.name) {
|
const lineHeight = 7;
|
||||||
doc.text(`Name: ${settings.name}`, margin, yPos);
|
function drawField(label, value) {
|
||||||
yPos += 7;
|
if (!value) return;
|
||||||
|
const lines = doc.splitTextToSize(`${label}: ${value}`, contentWidth);
|
||||||
|
doc.text(lines, margin, yPos);
|
||||||
|
yPos += lineHeight * lines.length;
|
||||||
}
|
}
|
||||||
if (settings && settings.adresse) {
|
if (settings) {
|
||||||
doc.text(`Adresse: ${settings.adresse}`, margin, yPos);
|
drawField('Name', settings.name);
|
||||||
yPos += 7;
|
drawField('Adresse', settings.adresse);
|
||||||
}
|
drawField('Kundennummer', settings.kundennummer);
|
||||||
if (settings && settings.kundennummer) {
|
|
||||||
doc.text(`Kundennummer: ${settings.kundennummer}`, margin, yPos);
|
|
||||||
yPos += 7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
yPos += 5;
|
yPos += 6;
|
||||||
|
|
||||||
// Summary text
|
// ---- Group entries by status (order kept in sync with views/index.ejs) ----
|
||||||
const totalApplications = applications.length;
|
|
||||||
const summaryText = year
|
|
||||||
? `Im Monat ${monthName} ${year} habe ich mich insgesamt auf ${totalApplications} Stellen beworben.`
|
|
||||||
: `Insgesamt habe ich mich auf ${totalApplications} Stellen beworben.`;
|
|
||||||
|
|
||||||
doc.setFont('helvetica', 'bold');
|
|
||||||
doc.text(summaryText, pageWidth / 2, yPos, { align: 'center' });
|
|
||||||
|
|
||||||
yPos += 12;
|
|
||||||
|
|
||||||
// Group entries by status (keep this order in sync with views/index.ejs)
|
|
||||||
const statusOrder = ['Gesendet', 'Eingangsbestätigung', 'Vorstellungsgespräch',
|
const statusOrder = ['Gesendet', 'Eingangsbestätigung', 'Vorstellungsgespräch',
|
||||||
'Einstellung', 'Absage', 'Keine Rückmeldung'];
|
'Einstellung', 'Absage', 'Keine Rückmeldung'];
|
||||||
|
// Accent colour per status (RGB) – reused by the chart and the list headings
|
||||||
|
const statusColors = {
|
||||||
|
'Gesendet': [59, 130, 246],
|
||||||
|
'Eingangsbestätigung': [14, 165, 233],
|
||||||
|
'Vorstellungsgespräch': [245, 158, 11],
|
||||||
|
'Einstellung': [34, 197, 94],
|
||||||
|
'Absage': [239, 68, 68],
|
||||||
|
'Keine Rückmeldung': [107, 114, 128],
|
||||||
|
'Ohne Status': [148, 163, 184]
|
||||||
|
};
|
||||||
|
const fallbackColor = [99, 102, 241];
|
||||||
|
const colorFor = (s) => statusColors[s] || fallbackColor;
|
||||||
|
|
||||||
const groups = {};
|
const groups = {};
|
||||||
applications.forEach((app) => {
|
applications.forEach((app) => {
|
||||||
const key = (app.status && app.status.trim()) ? app.status.trim() : 'Ohne Status';
|
const key = (app.status && app.status.trim()) ? app.status.trim() : 'Ohne Status';
|
||||||
@@ -360,6 +378,150 @@ function generatePdfDocument(settings, applications, month, year) {
|
|||||||
...Object.keys(groups).filter((k) => !statusOrder.includes(k))
|
...Object.keys(groups).filter((k) => !statusOrder.includes(k))
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const totalApplications = applications.length;
|
||||||
|
const countOf = (s) => (groups[s] ? groups[s].length : 0);
|
||||||
|
const pct = (n) => (totalApplications ? Math.round((n / totalApplications) * 100) : 0);
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Statistics overview: KPI cards + distribution charts
|
||||||
|
// ============================================================
|
||||||
|
function sectionHeading(text) {
|
||||||
|
ensureSpace(12);
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(12);
|
||||||
|
doc.setTextColor(31, 41, 55);
|
||||||
|
doc.text(text, margin, yPos);
|
||||||
|
yPos += 2.5;
|
||||||
|
doc.setDrawColor(209, 213, 219);
|
||||||
|
doc.setLineWidth(0.4);
|
||||||
|
doc.line(margin, yPos, margin + contentWidth, yPos);
|
||||||
|
yPos += 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
sectionHeading('Übersicht');
|
||||||
|
|
||||||
|
// --- KPI cards ---
|
||||||
|
const answered = totalApplications - countOf('Gesendet')
|
||||||
|
- countOf('Keine Rückmeldung') - countOf('Ohne Status');
|
||||||
|
const kpis = [
|
||||||
|
{ label: 'Bewerbungen', value: String(totalApplications), color: [55, 65, 81] },
|
||||||
|
{ label: 'Gespräche', value: String(countOf('Vorstellungsgespräch')), color: statusColors['Vorstellungsgespräch'] },
|
||||||
|
{ label: 'Einstellungen', value: String(countOf('Einstellung')), color: statusColors['Einstellung'] },
|
||||||
|
{ label: 'Absagen', value: String(countOf('Absage')), color: statusColors['Absage'] },
|
||||||
|
{ label: 'Antwortquote', value: pct(answered) + '%', color: [59, 130, 246] }
|
||||||
|
];
|
||||||
|
const cardGap = 3.5;
|
||||||
|
const cardW = (contentWidth - cardGap * (kpis.length - 1)) / kpis.length;
|
||||||
|
const cardH = 22;
|
||||||
|
ensureSpace(cardH + 4);
|
||||||
|
const cardTop = yPos;
|
||||||
|
kpis.forEach((kpi, i) => {
|
||||||
|
const x = margin + i * (cardW + cardGap);
|
||||||
|
doc.setFillColor(248, 250, 252);
|
||||||
|
doc.roundedRect(x, cardTop, cardW, cardH, 2, 2, 'F');
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(19);
|
||||||
|
doc.setTextColor(kpi.color[0], kpi.color[1], kpi.color[2]);
|
||||||
|
doc.text(kpi.value, x + cardW / 2, cardTop + 11, { align: 'center' });
|
||||||
|
doc.setFillColor(kpi.color[0], kpi.color[1], kpi.color[2]);
|
||||||
|
doc.roundedRect(x + cardW / 2 - 5, cardTop + 13.5, 10, 1.1, 0.5, 0.5, 'F');
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(8);
|
||||||
|
doc.setTextColor(107, 114, 128);
|
||||||
|
doc.text(kpi.label, x + cardW / 2, cardTop + 18.5, { align: 'center' });
|
||||||
|
});
|
||||||
|
yPos = cardTop + cardH + 10;
|
||||||
|
|
||||||
|
// --- Status distribution as a horizontal bar chart ---
|
||||||
|
if (totalApplications > 0) {
|
||||||
|
sectionHeading('Status-Verteilung');
|
||||||
|
const maxCount = Math.max(...orderedKeys.map((k) => groups[k].length));
|
||||||
|
const labelW = 46;
|
||||||
|
const valueW = 24;
|
||||||
|
const trackX = margin + labelW;
|
||||||
|
const trackW = contentWidth - labelW - valueW;
|
||||||
|
const rowH = 8;
|
||||||
|
orderedKeys.forEach((key) => {
|
||||||
|
const count = groups[key].length;
|
||||||
|
ensureSpace(rowH);
|
||||||
|
const barY = yPos + 1.4;
|
||||||
|
const barH = 5;
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(9);
|
||||||
|
doc.setTextColor(55, 65, 81);
|
||||||
|
doc.text(doc.splitTextToSize(key, labelW - 3)[0], margin, barY + barH - 1.2);
|
||||||
|
doc.setFillColor(237, 240, 244);
|
||||||
|
doc.roundedRect(trackX, barY, trackW, barH, 1, 1, 'F');
|
||||||
|
const c = colorFor(key);
|
||||||
|
const w = maxCount ? Math.max(1.5, (count / maxCount) * trackW) : 1.5;
|
||||||
|
doc.setFillColor(c[0], c[1], c[2]);
|
||||||
|
doc.roundedRect(trackX, barY, w, barH, 1, 1, 'F');
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(8.5);
|
||||||
|
doc.setTextColor(55, 65, 81);
|
||||||
|
doc.text(`${count} (${pct(count)}%)`, margin + contentWidth, barY + barH - 1.2, { align: 'right' });
|
||||||
|
yPos += rowH;
|
||||||
|
});
|
||||||
|
yPos += 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Application type (Art) as a single 100% stacked bar with legend ---
|
||||||
|
const artCounts = {};
|
||||||
|
applications.forEach((app) => {
|
||||||
|
const k = (app.art && app.art.trim()) ? app.art.trim() : 'Sonstige';
|
||||||
|
artCounts[k] = (artCounts[k] || 0) + 1;
|
||||||
|
});
|
||||||
|
const artKeys = Object.keys(artCounts);
|
||||||
|
if (totalApplications > 0 && artKeys.length > 0) {
|
||||||
|
const artPalette = [[37, 99, 235], [13, 148, 136], [217, 119, 6], [219, 39, 119],
|
||||||
|
[124, 58, 237], [5, 150, 105], [156, 163, 175]];
|
||||||
|
ensureSpace(24);
|
||||||
|
sectionHeading('Bewerbungsart');
|
||||||
|
const barY = yPos;
|
||||||
|
const barH = 7;
|
||||||
|
let cum = 0;
|
||||||
|
artKeys.forEach((key, i) => {
|
||||||
|
const seg = (artCounts[key] / totalApplications) * contentWidth;
|
||||||
|
const c = artPalette[i % artPalette.length];
|
||||||
|
doc.setFillColor(c[0], c[1], c[2]);
|
||||||
|
doc.rect(margin + cum, barY, seg, barH, 'F');
|
||||||
|
cum += seg;
|
||||||
|
if (i < artKeys.length - 1) {
|
||||||
|
doc.setFillColor(255, 255, 255);
|
||||||
|
doc.rect(margin + cum - 0.4, barY, 0.8, barH, 'F');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
yPos = barY + barH + 6;
|
||||||
|
// Legend (wraps across rows when needed)
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(8.5);
|
||||||
|
let lx = margin;
|
||||||
|
artKeys.forEach((key, i) => {
|
||||||
|
const c = artPalette[i % artPalette.length];
|
||||||
|
const text = `${key} (${artCounts[key]})`;
|
||||||
|
const itemW = 4.5 + doc.getTextWidth(text) + 7;
|
||||||
|
if (lx + itemW > margin + contentWidth) { lx = margin; yPos += 5.5; }
|
||||||
|
doc.setFillColor(c[0], c[1], c[2]);
|
||||||
|
doc.roundedRect(lx, yPos - 2.6, 3, 3, 0.6, 0.6, 'F');
|
||||||
|
doc.setTextColor(55, 65, 81);
|
||||||
|
doc.text(text, lx + 4.5, yPos);
|
||||||
|
lx += itemW;
|
||||||
|
});
|
||||||
|
yPos += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declarative summary sentence for the official record
|
||||||
|
ensureSpace(10);
|
||||||
|
doc.setFont('helvetica', 'italic');
|
||||||
|
doc.setFontSize(10.5);
|
||||||
|
doc.setTextColor(75, 85, 99);
|
||||||
|
const summaryText = (month || year)
|
||||||
|
? `Im Zeitraum ${period} habe ich mich auf ${totalApplications} Stellen beworben.`
|
||||||
|
: `Insgesamt habe ich mich auf ${totalApplications} Stellen beworben.`;
|
||||||
|
doc.text(summaryText, pageWidth / 2, yPos, { align: 'center' });
|
||||||
|
doc.setTextColor(0, 0, 0);
|
||||||
|
yPos += 12;
|
||||||
|
|
||||||
// One block per application — no table, so the note (which documents the
|
// One block per application — no table, so the note (which documents the
|
||||||
// full Verlauf) gets the entire page width and is shown completely.
|
// full Verlauf) gets the entire page width and is shown completely.
|
||||||
orderedKeys.forEach((statusKey) => {
|
orderedKeys.forEach((statusKey) => {
|
||||||
@@ -440,7 +602,8 @@ function generatePdfDocument(settings, applications, month, year) {
|
|||||||
yPos += 3;
|
yPos += 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notizen label
|
// Notizen — only rendered when the entry actually has notes
|
||||||
|
if (notizen) {
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.setFontSize(10);
|
doc.setFontSize(10);
|
||||||
doc.setTextColor(20, 20, 20);
|
doc.setTextColor(20, 20, 20);
|
||||||
@@ -452,8 +615,7 @@ function generatePdfDocument(settings, applications, month, year) {
|
|||||||
doc.setFontSize(10);
|
doc.setFontSize(10);
|
||||||
doc.setTextColor(0, 0, 0);
|
doc.setTextColor(0, 0, 0);
|
||||||
const lineHeight = 5;
|
const lineHeight = 5;
|
||||||
const noteText = notizen || '(keine Notizen)';
|
notizen.split(/\r?\n/).forEach((paragraph) => {
|
||||||
noteText.split(/\r?\n/).forEach((paragraph) => {
|
|
||||||
const lines = doc.splitTextToSize(paragraph.length ? paragraph : ' ', contentWidth - 4);
|
const lines = doc.splitTextToSize(paragraph.length ? paragraph : ' ', contentWidth - 4);
|
||||||
lines.forEach((line) => {
|
lines.forEach((line) => {
|
||||||
ensureSpace(lineHeight);
|
ensureSpace(lineHeight);
|
||||||
@@ -461,6 +623,7 @@ function generatePdfDocument(settings, applications, month, year) {
|
|||||||
yPos += lineHeight;
|
yPos += lineHeight;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Separator before the next entry
|
// Separator before the next entry
|
||||||
yPos += 4;
|
yPos += 4;
|
||||||
@@ -500,7 +663,7 @@ function loadPdfLibraries() {
|
|||||||
// Check if already loading
|
// Check if already loading
|
||||||
if (document.getElementById('jspdf-script')) {
|
if (document.getElementById('jspdf-script')) {
|
||||||
const checkLoaded = setInterval(() => {
|
const checkLoaded = setInterval(() => {
|
||||||
if (typeof jsPDF !== 'undefined') {
|
if (window.jspdf && window.jspdf.jsPDF) {
|
||||||
clearInterval(checkLoaded);
|
clearInterval(checkLoaded);
|
||||||
pdfLibrariesLoaded = true;
|
pdfLibrariesLoaded = true;
|
||||||
resolve();
|
resolve();
|
||||||
|
|||||||
+8
-6
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<%- include('partials/head') %>
|
<%- include('partials/head') %>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen transition-colors duration-300 bg-gray-50 dark:bg-gray-900" id="body">
|
<body class="min-h-screen transition-colors duration-300 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100" id="body">
|
||||||
<%- include('partials/header', { hideSettings: true }) %>
|
<%- include('partials/header', { hideSettings: true }) %>
|
||||||
|
|
||||||
<%
|
<%
|
||||||
@@ -191,23 +191,25 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
const body = document.getElementById('body');
|
// The `dark` class lives on <html> so Tailwind's `.dark .dark:*`
|
||||||
|
// selectors also style the <body> itself.
|
||||||
|
const root = document.documentElement;
|
||||||
const dm = localStorage.getItem('darkMode');
|
const dm = localStorage.getItem('darkMode');
|
||||||
if (dm === 'enabled' || (!dm && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
if (dm === 'enabled' || (!dm && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
body.classList.add('dark');
|
root.classList.add('dark');
|
||||||
}
|
}
|
||||||
const toggle = document.getElementById('darkModeToggle');
|
const toggle = document.getElementById('darkModeToggle');
|
||||||
const sun = document.getElementById('sunIcon');
|
const sun = document.getElementById('sunIcon');
|
||||||
const moon = document.getElementById('moonIcon');
|
const moon = document.getElementById('moonIcon');
|
||||||
function sync() {
|
function sync() {
|
||||||
const d = body.classList.contains('dark');
|
const d = root.classList.contains('dark');
|
||||||
if (sun) sun.classList.toggle('hidden', d);
|
if (sun) sun.classList.toggle('hidden', d);
|
||||||
if (moon) moon.classList.toggle('hidden', !d);
|
if (moon) moon.classList.toggle('hidden', !d);
|
||||||
}
|
}
|
||||||
sync();
|
sync();
|
||||||
if (toggle) toggle.addEventListener('click', () => {
|
if (toggle) toggle.addEventListener('click', () => {
|
||||||
body.classList.toggle('dark');
|
root.classList.toggle('dark');
|
||||||
localStorage.setItem('darkMode', body.classList.contains('dark') ? 'enabled' : 'disabled');
|
localStorage.setItem('darkMode', root.classList.contains('dark') ? 'enabled' : 'disabled');
|
||||||
sync();
|
sync();
|
||||||
});
|
});
|
||||||
const cy = document.getElementById('currentYear');
|
const cy = document.getElementById('currentYear');
|
||||||
|
|||||||
+3
-1
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<%- include('partials/head') %>
|
<%- include('partials/head') %>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen transition-colors duration-300" id="body">
|
<body class="min-h-screen transition-colors duration-300 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100" id="body">
|
||||||
<%- include('partials/header') %>
|
<%- include('partials/header') %>
|
||||||
|
|
||||||
<main class="container mx-auto px-4 py-8">
|
<main class="container mx-auto px-4 py-8">
|
||||||
@@ -57,6 +57,7 @@
|
|||||||
|
|
||||||
<div class="ml-auto">
|
<div class="ml-auto">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
id="exportPdfBtn"
|
id="exportPdfBtn"
|
||||||
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors flex items-center space-x-2">
|
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors flex items-center space-x-2">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -68,6 +69,7 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
id="addApplicationBtn"
|
id="addApplicationBtn"
|
||||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors flex items-center space-x-2">
|
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors flex items-center space-x-2">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|||||||
@@ -3,5 +3,9 @@
|
|||||||
<title>Bewerbungs-Tracker</title>
|
<title>Bewerbungs-Tracker</title>
|
||||||
<!-- Tailwind CSS CDN -->
|
<!-- Tailwind CSS CDN -->
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script>
|
||||||
|
// Toggle dark mode via the `.dark` class (default is `media`, i.e. OS-driven)
|
||||||
|
tailwind.config = { darkMode: 'class' };
|
||||||
|
</script>
|
||||||
<!-- Custom CSS -->
|
<!-- Custom CSS -->
|
||||||
<link rel="stylesheet" href="/css/styles.css">
|
<link rel="stylesheet" href="/css/styles.css">
|
||||||
|
|||||||
Reference in New Issue
Block a user