-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDlheVikendy.tsx
More file actions
185 lines (169 loc) · 9.34 KB
/
Copy pathDlheVikendy.tsx
File metadata and controls
185 lines (169 loc) · 9.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
import React, { useEffect, useMemo, useState } from 'react';
import { getLang, persistLang, type Lang } from './tools-i18n';
import { holidaysForYear, restHolidaySet, dkey, type Country } from './holidaysSkCz';
/* Nájde "mosty": okná dní, kde za cenu N dní dovolenky (pracovné dni v okne)
získaš súvislé voľno dlhé M dní. Okno musí začínať aj končiť voľným dňom
a obsahovať aspoň jeden sviatok (inak je to obyčajná dovolenka). */
interface Plan {
start: Date; end: Date;
cost: number; // vacation days needed
total: number; // total consecutive days off
days: Array<{ date: Date; kind: 'weekend' | 'holiday' | 'vacation' }>;
holidays: string[]; // names of holidays inside
}
function findBridges(year: number, country: Country, lang: Lang, maxCost: number): Plan[] {
const rest = restHolidaySet(year, country);
const hols = holidaysForYear(year, country).filter((h) => h.rest);
const holName = new Map(hols.map((h) => [dkey(h.date), lang === 'sk' ? h.sk : h.en]));
// build day array (with 10-day margins into neighbouring years)
const start = Date.UTC(year, 0, 1) - 10 * 86400000;
const nDays = 365 + 20 + 1;
const prevRest = restHolidaySet(year - 1, country);
const nextRest = restHolidaySet(year + 1, country);
const isOff: boolean[] = [];
const dates: Date[] = [];
for (let i = 0; i < nDays; i++) {
const d = new Date(start + i * 86400000);
dates.push(d);
const dow = d.getUTCDay();
const k = dkey(d);
isOff.push(dow === 0 || dow === 6 || rest.has(k) || prevRest.has(k) || nextRest.has(k));
}
const cands: Plan[] = [];
const MAXLEN = 17;
for (let i = 0; i < nDays; i++) {
if (!isOff[i] || (i > 0 && isOff[i - 1])) continue; // window starts at the beginning of an off-run
for (let j = i; j < Math.min(i + MAXLEN, nDays); j++) {
if (!isOff[j]) continue;
if (j + 1 < nDays && isOff[j + 1]) continue; // extend to the end of its off-run
let cost = 0;
let holCount = 0;
const days: Plan['days'] = [];
for (let x = i; x <= j; x++) {
const d = dates[x];
const k = dkey(d);
if (!isOff[x]) { cost++; days.push({ date: d, kind: 'vacation' }); }
else if (holName.has(k) || prevRest.has(k) || nextRest.has(k)) { holCount++; days.push({ date: d, kind: 'holiday' }); }
else days.push({ date: d, kind: 'weekend' });
}
if (cost < 1 || cost > maxCost || holCount === 0) continue;
const total = j - i + 1;
if (total < cost + 3) continue; // worth it: e.g. 1 vacation day → 4+ days off
// the plan must belong to this year (majority of days)
if (dates[i].getUTCFullYear() !== year && dates[j].getUTCFullYear() !== year) continue;
const names = [...new Set(days.filter((d) => d.kind === 'holiday').map((d) => holName.get(dkey(d.date)) || '').filter(Boolean))];
cands.push({ start: dates[i], end: dates[j], cost, total, days, holidays: names });
}
}
// pick best non-overlapping by efficiency, then order by date
cands.sort((a, b) => (b.total / b.cost) - (a.total / a.cost) || b.total - a.total);
const picked: Plan[] = [];
for (const c of cands) {
if (picked.some((p) => c.start <= p.end && c.end >= p.start)) continue;
picked.push(c);
}
picked.sort((a, b) => a.start.getTime() - b.start.getTime());
return picked;
}
export default function DlheVikendy() {
const [lang, setLang] = useState<Lang>(getLang());
const t = (sk: string, en: string) => (lang === 'sk' ? sk : en);
useEffect(() => { persistLang(lang); }, [lang]);
const thisYear = new Date().getUTCFullYear();
const [country, setCountry] = useState<Country>('sk');
const [year, setYear] = useState(thisYear);
const [maxCost, setMaxCost] = useState(4);
const plans = useMemo(() => findBridges(year, country, lang, maxCost), [year, country, lang, maxCost]);
const hols = useMemo(() => holidaysForYear(year, country), [year, country]);
const totalVac = plans.reduce((s, p) => s + p.cost, 0);
const totalOff = plans.reduce((s, p) => s + p.total, 0);
const loc = lang === 'sk' ? 'sk-SK' : 'en-GB';
const fmtD = (d: Date) => d.toLocaleDateString(loc, { day: 'numeric', month: 'numeric', timeZone: 'UTC' });
const fmtDL = (d: Date) => d.toLocaleDateString(loc, { weekday: 'short', day: 'numeric', month: 'long', timeZone: 'UTC' });
const now = new Date();
const isPast = (d: Date) => d.getTime() < Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
return (
<div className="dv-app">
<header className="dv-top">
<div className="dv-logo"><span className="sym" aria-hidden="true">☀</span>{t('Dlhé víkendy', 'Long weekends')} <span className="yr">{year}</span></div>
<span className="dv-spacer" />
<div className="lang-switch">
<button className={lang === 'sk' ? 'on' : ''} onClick={() => setLang('sk')}>SK</button>
<button className={lang === 'en' ? 'on' : ''} onClick={() => setLang('en')}>EN</button>
</div>
<a className="dv-back" href="/#tools">← {t('Portfólio', 'Portfolio')}</a>
</header>
<div className="dv-controls">
<div className="dv-seg" role="tablist" aria-label={t('Krajina', 'Country')}>
<button role="tab" aria-selected={country === 'sk'} className={country === 'sk' ? 'on' : ''} onClick={() => setCountry('sk')}>🇸🇰 Slovensko</button>
<button role="tab" aria-selected={country === 'cz'} className={country === 'cz' ? 'on' : ''} onClick={() => setCountry('cz')}>🇨🇿 Česko</button>
</div>
<div className="dv-years">
{[thisYear, thisYear + 1, thisYear + 2].map((y) => (
<button key={y} className={y === year ? 'on' : ''} onClick={() => setYear(y)}>{y}</button>
))}
</div>
<div className="dv-maxc">
<span>{t('Max. dní dovolenky na jeden most:', 'Max vacation days per bridge:')}</span>
{[1, 2, 3, 4, 5].map((n) => (
<button key={n} className={n === maxCost ? 'on' : ''} onClick={() => setMaxCost(n)}>{n}</button>
))}
</div>
</div>
<div className="dv-summary">
{t(
`Za ${totalVac} dní dovolenky získaš spolu ${totalOff} dní voľna v ${plans.length} blokoch.`,
`${totalVac} vacation days buy you ${totalOff} days off in ${plans.length} blocks.`
)}
</div>
<div className="dv-plans">
{plans.map((p, i) => (
<article key={i} className={`dv-plan ${year === thisYear && isPast(p.end) ? 'past' : ''}`}>
<div className="dv-plan-head">
<div className="range">
<strong>{fmtDL(p.start)} – {fmtDL(p.end)}</strong>
<span className="hols">{p.holidays.join(' · ')}</span>
</div>
<div className="score">
<span className="deal">{p.cost} {t(p.cost === 1 ? 'deň dovolenky' : p.cost < 5 ? 'dni dovolenky' : 'dní dovolenky', p.cost === 1 ? 'vacation day' : 'vacation days')}</span>
<span className="arrow" aria-hidden="true">→</span>
<span className="gain">{p.total} {t('dní voľna', 'days off')}</span>
<span className="mult">{(p.total / p.cost).toFixed(1)}×</span>
</div>
</div>
<div className="dv-strip" role="img" aria-label={t('Vizualizácia dní', 'Day visualization')}>
{p.days.map((d, x) => (
<span key={x} className={`cell ${d.kind}`} title={`${fmtD(d.date)} — ${d.kind === 'vacation' ? t('dovolenka', 'vacation') : d.kind === 'holiday' ? t('sviatok', 'holiday') : t('víkend', 'weekend')}`}>
<em>{d.date.getUTCDate()}</em>
{d.kind === 'vacation' ? 'D' : ''}
</span>
))}
</div>
</article>
))}
{plans.length === 0 && <p className="dv-empty">{t('Pre zvolený filter sa nenašli žiadne mosty.', 'No bridges found for the chosen filter.')}</p>}
</div>
<div className="dv-legend">
<span><i className="sw weekend" /> {t('víkend', 'weekend')}</span>
<span><i className="sw holiday" /> {t('sviatok', 'holiday')}</span>
<span><i className="sw vacation" /> {t('dovolenka (D)', 'vacation (D)')}</span>
</div>
<section className="dv-hols">
<h2>{t(`Sviatky ${country === 'sk' ? 'na Slovensku' : 'v Česku'} v roku ${year}`, `Public holidays in ${country === 'sk' ? 'Slovakia' : 'Czechia'} ${year}`)}</h2>
<div className="list">
{hols.map((h) => (
<div key={h.sk + h.date.getTime()} className={`row ${h.rest ? '' : 'work'}`}>
<span className="d">{h.date.getUTCDate()}. {h.date.toLocaleDateString(loc, { month: 'short', timeZone: 'UTC' })}</span>
<span className="dow">{h.date.toLocaleDateString(loc, { weekday: 'long', timeZone: 'UTC' })}</span>
<span className="nm">{lang === 'sk' ? h.sk : h.en}</span>
{!h.rest && <span className="tag">{t('pracovný deň', 'working day')}</span>}
</div>
))}
</div>
{country === 'sk' && year === 2026 && (
<p className="dv-note">{t('8. máj a 15. september sú v roku 2026 sviatkami, ale pracovnými dňami (zák. 261/2025) — v mostoch preto nefigurujú.', 'In 2026, 8 May and 15 September are holidays but working days (Act 261/2025), so they are not part of any bridge.')}</p>
)}
</section>
</div>
);
}