IlmHamroh
JavaScript Full-stack/21-qism. React: media, integratsiyalar, PWA1/32-dars29 daqiqa
Mundarija (39)

React kalendar va bron qilish UI: oy setkasi, sanalar oralig'i, vaqt slotlari va kutubxonalar

Qisqacha: Kalendar — sanalar jadvali: oy 7 ustunli setkaga joylanadi, har kun tugma bo'ladi, band kunlar o'chiriladi. Bron esa ikki qadam: sana (yoki sanalar oralig'i), keyin vaqt sloti — bo'sh stol bor-yo'qligi yarim ochiq oraliqlar [boshi, oxiri) bilan tekshiriladi. Vaqt restoran zonasida ko'rsatiladi, serverga lahza (UTC) bo'lib boradi. Tayyor kutubxonalar: sana tanlash uchun react-day-picker (+19.6 KB gzip), oy/hafta jadvali uchun react-big-calendar (+58.6), FullCalendar (+56.5) yoki Schedule-X (+47.5).

Bu darsda

  • Oy setkasini kutubxonasiz hisoblaysiz: oyda necha kun, 1-kun haftaning qaysi kuni, nega Date.UTC.
  • Kalendarni klaviatura bilan boshqariladigan qilasiz: roving tabindex, strelkalar, aria-disabled.
  • Sanalar oralig'ini (kelish — ketish) va vaqt slotlarini band joylarni hisobga olib tanlaysiz.
  • Bron vaqtini restoran zonasida ko'rsatasiz va serverga to'g'ri lahza yuborasiz.
  • To'rt kutubxonani (react-day-picker, react-big-calendar, FullCalendar, Schedule-X) vazifasi, hajmi va CSP natijasi bo'yicha tanlaysiz.

Oldin bilishingiz kerak: Date va vaqt zonalari, Temporal API va sana kutubxonalari, Intl.DateTimeFormat, Maxsus inputlar: sana, Fokus boshqaruvi va klaviatura navigatsiyasi.

1. Nega bu kerak?

O'tgan darsda 3D sahnalar bilan 20-qismni yopdik. 21-qism — real ilovalarda doim so'raladigan "integratsiyalar": kalendar, tarjima, grafik, xarita, to'lov.

«Bahor»ga mehmonlar qo'ng'iroq qilib stol band qilishadi. Jasur aka daftarga yozadi: "20-oktabr, 19:00, 4 kishi". Ikki muammo bor. Kechqurun telefon to'xtamaydi. Daftarda esa bir stolga ikki mehmon yozilib qoladi — 19:00 dagi bron 20:00 dagi bilan ustma-ust tushganini hech kim sezmaydi.

Jasur aka saytda bron (booking — oldindan joy band qilish) sahifasini so'radi. Mehmon kunni tanlaydi, keyin bo'sh vaqtni. Band kunlar va to'lgan vaqtlar tanlanmaydi.

Maxsus inputlar darsida <input type="date"> va uning UTC tuzog'ini ko'rgan edik. U yerda "band kunlarni kulrang qilish, oraliq — kalendar kutubxonasi ishi" degan edik. Bugun shu va'dani bajaramiz. Avval g'oyani kutubxonasiz quramiz, keyin tayyor kutubxonalarni o'lchov bilan solishtiramiz.

sequenceDiagram
  participant M as Mehmon
  participant K as Kalendar
  participant S as Server
  M->>K: 20-oktabrni tanlaydi
  K->>S: shu kun bandligi?
  S-->>K: band bronlar ro'yxati
  K->>M: bo'sh vaqtlar: 17:00, 19:00
  M->>S: 19:00, 4 kishi
  S-->>M: tasdiq yoki "band bo'lib qoldi"

Nimaga qarang: kalendar faqat taklif qiladi. Oxirgi so'z — serverda. Ikki mehmon bir vaqtda bir slotni bosishi mumkin, buni faqat server ajrata oladi.

2. Oy setkasi: kalendar ichida nima bor

2.1 Uchta savol

Har oylik kalendar uchta savolga javob beradi:

  1. Oyda necha kun bor (28, 29, 30 yoki 31)?
  2. 1-kun haftaning qaysi kuniga tushadi?
  3. Hafta qaysi kundan boshlanadi? O'zbekistonda va Rossiyada — dushanbadan, AQShda — yakshanbadan.

Javoblar tayyor bo'lsa, oy — shunchaki 7 ustunli jadval. Boshida va oxirida bo'sh kataklar qoladi.

2.2 O'quv modeli: setkani hisoblash

Quyidagi — o'quv modeli, hech qaysi kutubxonaning kodi emas. Natija oynasida kalendar kutubxonalari yo'q, lekin g'oya shu:

jsx
// O'quv modeli: oy setkasi — kutubxonasiz
const WEEKDAYS = ["Du", "Se", "Ch", "Pa", "Ju", "Sh", "Ya"];

function daysInMonth(year, month) {
  // keyingi oyning 0-kuni = shu oyning oxirgi kuni
  return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
}

function firstWeekday(year, month) {
  // getUTCDay: 0 = yakshanba. Bizda hafta dushanbadan: 0 = Du
  const day = new Date(Date.UTC(year, month, 1)).getUTCDay();
  return (day + 6) % 7;
}

function monthWeeks(year, month) {
  const cells = Array(firstWeekday(year, month)).fill(null);
  for (let d = 1; d <= daysInMonth(year, month); d++) {
    cells.push(d);
  }
  while (cells.length % 7 !== 0) cells.push(null);
  const weeks = [];
  for (let i = 0; i < cells.length; i += 7) {
    weeks.push(cells.slice(i, i + 7));
  }
  return weeks;
}

export default function App() {
  const weeks = monthWeeks(2026, 9); // 9 — oktabr (oylar 0 dan)
  console.log(weeks.length, weeks[0]);
  return (
    <table>
      <caption>Oktabr 2026</caption>
      <thead>
        <tr>
          {WEEKDAYS.map((w) => (
            <th key={w}>{w}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {weeks.map((week, i) => (
          <tr key={i}>
            {week.map((d, j) => (
              <td key={j}>{d ?? "·"}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

Konsolda:

text
5 (7) [null, null, null, 1, 2, 3, 4]

Ekranda:

text
Oktabr 2026
Du Se Ch Pa Ju Sh Ya
· · · 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 ·

Oktabr 2026 — 5 hafta qatori. 1-oktabr payshanbaga tushdi, shuning uchun boshida uchta bo'sh katak.

Uch nozik joy:

  • month + 1, 0 — "keyingi oyning nolinchi kuni". Date uni oldingi kunga, ya'ni shu oyning oxirgi kuniga aylantiradi. Kabisa yilini (fevral 29) ham o'zi biladi.
  • (day + 6) % 7 — getUTCDay yakshanbani 0 deb sanaydi. Biz haftani dushanbadan boshlaymiz: yakshanba 6 bo'ladi, dushanba 0.
  • Date.UTC — hisob soat mintaqasiga bog'liq bo'lmasin. new Date(2026, 9, 1) mahalliy vaqtda yasaladi. Date va vaqt zonalari darsida ko'rganimizdek, ba'zi zonalarda yarim tun boshqa sanaga "sakrab" ketishi mumkin. Biz faqat kalendar sanasini hisoblayapmiz — soat kerak emas, shuning uchun hammasi UTC'da.

Tekshirib ko'ring: daysInMonth(2028, 1) nima qaytaradi?

Javob

29. month = 1 — fevral (oylar 0 dan). Date.UTC(2028, 2, 0) — "martning 0-kuni", ya'ni fevralning oxirgi kuni. 2028 — kabisa yili, shuning uchun 29. Qo'lda "fevral — 28" deb yozilgan jadval to'rt yilda bir marta xato qiladi, Date esa yo'q.

2.3 Sanani satr qilib saqlash

Tanlangan sanani state'da qanday saqlaymiz? Date obyekti emas, "2026-10-20" satri. Uch sabab:

  • Satrlarni === bilan solishtirish mumkin. Ikkita Date obyekti esa bir xil lahza bo'lsa ham === da false (obyektlar havola bo'yicha solishtiriladi).
  • "YYYY-MM-DD" satrlari < bilan to'g'ri tartiblanadi — "o'tgan kunmi?" degan savol bitta taqqoslash.
  • Serverga ham aynan shunday yuboriladi. Zona yo'q, UTC tuzog'i yo'q.

Buni Maxsus inputlar darsida forma uchun ko'rgan edik. Kalendarda ham qoida o'sha.

Oy nomlarini o'zimiz yozamiz. Intl o'zbekcha nomlarni berishi kerak edi, lekin kanon tekshiruvi topdi: Chrome 155 da Intl.DateTimeFormat("uz") sanani «2026 M10 17» ko'rinishida chiqaradi (Node'da esa to'g'ri). Bu muammo va uning yechimi — i18n II darsida.

3. Sana tanlash: tugmalar, band kunlar va klaviatura

3.1 Har kun — tugma

Kalendar o'qish uchun emas, bosish uchun. Demak har kun — <button>. O'tgan kunlar va band kunlar tanlanmasin:

jsx
import { useState } from "react";

const MONTHS = ["yanvar", "fevral", "mart", "aprel", "may", "iyun",
  "iyul", "avgust", "sentabr", "oktabr", "noyabr", "dekabr"];
const pad = (n) => String(n).padStart(2, "0");
const toIso = (y, m, d) => `${y}-${pad(m + 1)}-${pad(d)}`;

const TODAY = "2026-10-17"; // misol har kuni bir xil chiqsin
const BOOKED = ["2026-10-21", "2026-10-22"]; // tadbir kunlari

function isDisabled(date) {
  return date < TODAY || BOOKED.includes(date);
}

function DatePicker({ year, month, isSelected, onPick }) {
  return (
    <table>
      <caption>{`${MONTHS[month]} ${year}`}</caption>
      <tbody>
        {monthWeeks(year, month).map((week, i) => (
          <tr key={i}>
            {week.map((d, j) => {
              if (d === null) return <td key={j} />;
              const date = toIso(year, month, d);
              return (
                <td key={j}>
                  <button
                    type="button"
                    disabled={isDisabled(date)}
                    aria-pressed={isSelected(date)}
                    onClick={() => onPick(date)}
                  >
                    {d}
                  </button>
                </td>
              );
            })}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

export default function App() {
  const [date, setDate] = useState(null);
  return (
    <>
      <DatePicker
        year={2026}
        month={9}
        isSelected={(d) => d === date}
        onPick={setDate}
      />
      <p>{date ? `Tanlandi: ${date}` : "Sana tanlang"}</p>
    </>
  );
}

Ekranda:

text
oktabr 2026
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
Tanlandi: 2026-10-20

«Ishga tushir» ni bosing va 21-ni bosib ko'ring — hech narsa bo'lmaydi: tugma disabled. aria-pressed ekran o'quvchiga "tanlangan" holatni aytadi — Malika 20-ni "bosilgan" deb eshitadi.

Bu blok davomi — oldingi blokdagi monthWeeks shu yerda ham ishlaydi. isSelected va onPick — funksiya prop'lar. Shu sababli bitta DatePicker ham bitta sana, ham oraliq uchun ishlaydi (keyingi bo'limlarda).

3.2 Klaviatura: 42 ta Tab emas

Bobur faqat klaviatura bilan ishlaydi. Yuqoridagi kalendarda u Tab bosib har kundan o'tadi — oktabr oxiriga yetish uchun 15 marta. Kalendardan chiqish uchun esa yana 15 marta.

ARIA Authoring Practices dagi sana tanlash naqshi buni boshqacha hal qiladi:

Tugma Nima qiladi
Tab kalendarga bitta to'xtash, keyingi Tab — undan chiqish
← → bir kun oldin / keyin
↑ ↓ bir hafta oldin / keyin
Enter, Space tanlash

Bunga roving tabindex (ko'chib yuruvchi fokus, Fokus boshqaruvi darsida ko'rgan edik) yordam beradi: faqat bitta kunning tabIndex i 0, qolganlari −1. Strelka bosilganda "0" boshqa kunga ko'chadi va fokus o'sha tugmaga o'tadi.

Yana bitta o'zgarish. disabled tugma fokus ololmaydi — strelka 21-ga kelib to'xtab qoladi. Shuning uchun band kun aria-disabled="true" oladi. U fokus oladi, ekran o'quvchi "band" deb o'qiydi, lekin bosilganda tanlanmaydi:

jsx
import { useEffect, useRef, useState } from "react";

const DAY_MS = 86_400_000;
const STEPS = {
  ArrowLeft: -1,
  ArrowRight: 1,
  ArrowUp: -7,
  ArrowDown: 7,
};

function addDays(date, n) {
  // "YYYY-MM-DD" — UTC yarim tuni; +n kun, yana satr
  const time = Date.parse(date) + n * DAY_MS;
  return new Date(time).toISOString().slice(0, 10);
}

function DatePicker({ year, month, isSelected, onPick }) {
  const [focused, setFocused] = useState(toIso(year, month, 1));
  const buttons = useRef(new Map());
  const byKeyboard = useRef(false);

  useEffect(() => {
    if (byKeyboard.current) buttons.current.get(focused)?.focus();
  }, [focused]);

  function onKeyDown(event) {
    const step = STEPS[event.key];
    if (step === undefined) return; // Enter/Space — tugmaning o'zi
    event.preventDefault(); // sahifa aylanib ketmasin
    const next = addDays(focused, step);
    if (!next.startsWith(`${year}-${pad(month + 1)}`)) return;
    byKeyboard.current = true;
    setFocused(next);
  }

  return (
    <table onKeyDown={onKeyDown}>
      <caption>{`${MONTHS[month]} ${year}`}</caption>
      <tbody>
        {monthWeeks(year, month).map((week, i) => (
          <tr key={i}>
            {week.map((d, j) => {
              if (d === null) return <td key={j} />;
              const date = toIso(year, month, d);
              const off = isDisabled(date);
              const name = `${d}-${MONTHS[month]}`;
              const label = off ? `${name}, band` : name;
              return (
                <td key={j}>
                  <button
                    type="button"
                    ref={(el) => {
                      buttons.current.set(date, el);
                    }}
                    tabIndex={date === focused ? 0 : -1}
                    aria-disabled={off || undefined}
                    aria-pressed={isSelected(date)}
                    aria-label={label}
                    onFocus={() => console.log("fokus:", label)}
                    onClick={() => {
                      setFocused(date);
                      if (!off) onPick(date);
                    }}
                  >
                    {d}
                  </button>
                </td>
              );
            })}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

export default function App() {
  const [date, setDate] = useState(null);
  useEffect(() => {
    if (date) console.log("tanlandi:", date);
  }, [date]);
  return (
    <DatePicker
      year={2026}
      month={9}
      isSelected={(d) => d === date}
      onPick={setDate}
    />
  );
}

Konsolda:

text
fokus: 20-oktabr
tanlandi: 2026-10-20
fokus: 21-oktabr, band
fokus: 28-oktabr
tanlandi: 2026-10-28

Qadamma-qadam:

Qadam Fokus Tanlangan
20-ni bosish 20 2026-10-20
→ 21 (band) o'zgarmadi
Enter 21 o'zgarmadi — aria-disabled
↓ 28 o'zgarmadi
Enter 28 2026-10-28

Nimaga qarang: Enter 21-da hech narsa tanlamadi, lekin fokus u yerga keldi va Malika "21-oktabr, band" ni eshitdi. disabled bo'lsa, u bu kunni umuman bilmay qolardi.

byKeyboard ref'i nega kerak? Birinchi chizishda effekt ham ishlaydi. Usiz sahifa ochilishi bilan fokus kalendarga "sakrab" ketardi — bu foydalanuvchini adashtiradi. Fokusni faqat strelka bosilgandan keyin ko'chiramiz.

O'quv modelimizda strelka oy chegarasida to'xtaydi. Haqiqiy kutubxonalar keyingi oyga o'tadi, PageUp/PageDown bilan oyni, Home/End bilan haftaning boshi-oxirini almashtiradi. Bular — tayyor kutubxona tejaydigan mehnat.

Tekshirib ko'ring: Nega tabIndex hamma kunda 0 emas?

Javob

Hammasida 0 bo'lsa, har kun Tab to'xtash joyi bo'ladi. Bobur kalendardan o'tish uchun 31 marta Tab bosadi. Roving tabindex'da kalendar bitta to'xtash: Tab — ichkariga, strelkalar — ichida yurish, yana Tab — chiqish. Jadval, menyu va tablar ham shu naqshda ishlaydi.

4. Sanalar oralig'i: kelish — ketish

4.1 Oraliq qanday tanlanadi

Sardor (stajyor) ta'til so'ramoqchi: 23-oktabrdan 26-oktabrgacha. Mehmonxona va dacha bronlari ham shunday ishlaydi. Bu — sanalar oralig'i (date range): ikki bosish, boshi va oxiri.

Qoidalar:

  1. Birinchi bosish — oraliq boshi.
  2. Ikkinchi bosish — oxiri. Agar u boshidan oldin bo'lsa, yangi boshlanish deb olinadi.
  3. Oraliq ichiga band kun tushsa — tanlov qaytadan boshlanadi. «Bahor»da 21–22-oktabr katta tadbir, hamma ishda.
  4. Oraliq to'liq bo'lsa, keyingi bosish yangi oraliqni boshlaydi.
jsx
function pickRange(range, date) {
  if (range.from === null || range.to !== null) {
    return { from: date, to: null };
  }
  if (date < range.from) return { from: date, to: null };
  for (let d = range.from; d <= date; d = addDays(d, 1)) {
    if (isDisabled(d)) return { from: date, to: null };
  }
  return { from: range.from, to: date };
}

function countDays({ from, to }) {
  return (Date.parse(to) - Date.parse(from)) / DAY_MS + 1;
}

export default function App() {
  const [range, setRange] = useState({ from: null, to: null });
  useEffect(() => {
    if (range.to) {
      const days = countDays(range);
      console.log(`${range.from} — ${range.to}: ${days} kun`);
    } else if (range.from) {
      console.log(`boshi: ${range.from}`);
    }
  }, [range]);
  const inRange = (d) =>
    d === range.from ||
    (range.to !== null && range.from <= d && d <= range.to);
  return (
    <DatePicker
      year={2026}
      month={9}
      isSelected={inRange}
      onPick={(d) => setRange((r) => pickRange(r, d))}
    />
  );
}

Konsolda:

text
fokus: 23-oktabr
boshi: 2026-10-23
fokus: 26-oktabr
2026-10-23 — 2026-10-26: 4 kun
fokus: 19-oktabr
boshi: 2026-10-19
fokus: 24-oktabr
boshi: 2026-10-24

Birinchi oraliq tayyor: 23–26, 4 kun. Keyin 19 → 24 tanlandi, lekin ular orasida 21–22 band. 3-qoida ishladi: 24 yangi boshlanish bo'ldi. O'tgan kunlar (17-dan oldin) esa umuman bosilmaydi — isDisabled ularni ham o'chiradi. Malika uchun ham har kunda aria-pressed bor — oraliqdagi har kun "bosilgan".

setRange((r) => pickRange(r, d)) — funksiyali yangilash (17-qism): yangi oraliq har doim eng so'nggi state'dan hisoblanadi.

4.2 Oxiri ichidami yoki tashqarisidami?

Sardorning ta'tili "23 dan 26 gacha" — 26 ham ta'til. Bu yopiq oraliq: ikkala chet ichida, countDays ga + 1 shuning uchun.

Mehmonxonada esa "23 dan 26 gacha" — 3 kecha. 26-kuni mehmon chiqib ketadi va xona boshqa mehmonga bo'sh. Bu yarim ochiq oraliq (half-open interval) [23, 26): boshi ichida, oxiri tashqarida. Intervallar darsidagi o'sha tushuncha.

Kutubxonalar odatda ikkinchisini ishlatadi. FullCalendar'ning select hodisasida end — eksklyuziv (hujjatda shunday yozilgan): payshanba bilan tugaydigan butun kunlik tanlovda end — juma. Kalendar kutubxonasini ulayotganda birinchi savol: "end ichidami?"

Tekshirib ko'ring: Mehmonxona bronida oraliq [2026-10-23, 2026-10-26). Keyingi mehmon 26-oktabrdan joy olsa, bronlar ustma-ust tushadimi?

Javob

Yo'q. Yarim ochiq oraliqda 26 birinchi bronga kirmaydi — u faqat ikkinchisining boshi. Ikki oraliq a.boshi < b.oxiri && b.boshi < a.oxiri bo'lsa kesishadi: b.boshi < a.oxiri — "2026-10-26" < "2026-10-26" — false. Yopiq oraliqda esa (Sardorning ta'tili kabi) 26 ikkalasida ham bo'lardi.

5. Vaqt slotlari: bo'sh stol bormi?

5.1 Slot nima

Sana tanlandi. Endi soat. «Bahor»ning kechki bronlari 17:00 dan 22:00 gacha, har bron 2 soat, boshlanishi har 30 daqiqada. Bu bo'laklar — vaqt slotlari (time slot): oldindan bo'lingan, tanlanadigan vaqt oralig'i. Tish shifokori, sartaroshxona, avtoservis bronlari ham shunday.

"Slot" so'zini React'da boshqa ma'noda ham uchratgansiz — komponent ichidagi bo'sh joy (compound komponentlar). Bu darsda "slot" — faqat vaqt bo'lagi.

Zalda uchta stol. 20-oktabr uchun bronlar allaqachon bor. Har slot uchun savol: shu 2 soat ichida qaysi stollar bo'sh? Stol bo'sh — agar uning hech bir broni slot bilan kesishmasa. Kesishish — o'sha yarim ochiq oraliq formulasi.

jsx
import { useState } from "react";

const toMin = (hhmm) => {
  const [h, m] = hhmm.split(":").map(Number);
  return h * 60 + m;
};
const toHhmm = (min) =>
  `${String(Math.floor(min / 60)).padStart(2, "0")}:` +
  String(min % 60).padStart(2, "0");

const OPEN = toMin("17:00");
const CLOSE = toMin("22:00");
const STEP = 30; // har 30 daqiqada boshlanadi
const LENGTH = 120; // bron 2 soat
const TABLES = [1, 2, 3];
const BOOKINGS = [
  { table: 1, start: "18:00", end: "20:00" },
  { table: 1, start: "20:00", end: "22:00" },
  { table: 2, start: "19:00", end: "21:00" },
  { table: 3, start: "17:00", end: "19:00" },
];

// [a, b) va [c, d) kesishadimi — yarim ochiq oraliqlar
const overlaps = (a, b, c, d) => a < d && c < b;

function freeTables(start) {
  const end = start + LENGTH;
  return TABLES.filter(
    (t) =>
      !BOOKINGS.some(
        (b) =>
          b.table === t &&
          overlaps(start, end, toMin(b.start), toMin(b.end)),
      ),
  );
}

export default function App() {
  const [picked, setPicked] = useState(null);
  const slots = [];
  for (let t = OPEN; t + LENGTH <= CLOSE; t += STEP) slots.push(t);
  return (
    <>
      <ul>
        {slots.map((t) => {
          const free = freeTables(t);
          return (
            <li key={t}>
              <button
                type="button"
                disabled={free.length === 0}
                onClick={() => setPicked({ t, table: free[0] })}
              >
                {toHhmm(t)}
              </button>
              {` bo'sh stol: ${free.length}`}
            </li>
          );
        })}
      </ul>
      <p>
        {picked
          ? `Tanlandi: ${toHhmm(picked.t)}–` +
            `${toHhmm(picked.t + LENGTH)}, ${picked.table}-stol`
          : "Vaqt tanlang"}
      </p>
    </>
  );
}

Ekranda:

text
17:00 bo'sh stol: 1
17:30 bo'sh stol: 0
18:00 bo'sh stol: 0
18:30 bo'sh stol: 0
19:00 bo'sh stol: 1
19:30 bo'sh stol: 1
20:00 bo'sh stol: 1
Tanlandi: 19:00–21:00, 3-stol

Ikki joyga qarang:

  • 19:00 — 3-stol bo'sh. Uning broni 17:00–19:00. Yarim ochiq oraliqda 19:00 eski bronga kirmaydi: 19:00 < 19:00 — false. Yopiq oraliq bilan hisoblasak, 19:00 "band" chiqardi va restoran bitta bronni bekorga yo'qotardi.
  • Oxirgi slot — 20:00. t + LENGTH <= CLOSE sharti: 20:30 dan boshlangan bron 22:30 da tugardi, oshxona esa 22:00 da yopiladi.

free[0] — eng kichik raqamli bo'sh stol. Haqiqiy restoranda stol kishi soniga qarab tanlanadi: 2 kishiga 6 o'rinli stol berilmaydi. Buni 3-mashqda qilasiz.

5.2 Kalendarning "hafta ko'rinishi"

Mehmonga slot ro'yxati yetadi. Jasur akaga esa boshqa ko'rinish kerak: hafta bo'yicha jadval, ustunlar — kunlar, qatorlar — soatlar, ichida bronlar to'rtburchak bo'lib turadi. Bu Google Calendar ko'rinishi. Uni qo'lda yozish — bir necha kunlik ish: ustma-ust tushgan bronlarni yonma-yon joylash, sudrab ko'chirish, chetidan cho'zish. Aynan shu yerda tayyor kutubxona o'zini oqlaydi — «Kutubxonalar» bo'limida ko'ramiz.

6. Vaqt zonasi: 19:00 kimning soati?

6.1 Bron — lahza

«Bahor» Toshkentda. Toshkent — UTC+5, yozgi vaqtga o'tish yo'q. Mehmon esa Moskvada (UTC+3) turib, onasiga stol band qilyapti. U ekranda "19:00" ni ko'radi. Bu Toshkent soati bo'lishi kerak — ovqat Toshkentda yeyiladi.

Qoida: slotlar joy (restoran) zonasida ko'rsatiladi, serverga lahza bo'lib boradi. Lahza — UTC'dagi aniq nuqta (Date va vaqt zonalari):

js
// Bron: 20-oktabr, 19:00 — «Bahor» vaqti (Toshkent, UTC+5)
const booking = new Date("2026-10-20T19:00:00+05:00");
console.log(booking.toISOString()); // 2026-10-20T14:00:00.000Z

function timeIn(date, timeZone) {
  return new Intl.DateTimeFormat("en-GB", {
    timeZone,
    hour: "2-digit",
    minute: "2-digit",
  }).format(date);
}

console.log(timeIn(booking, "Asia/Tashkent")); // 19:00
console.log(timeIn(booking, "Europe/Moscow")); // 17:00

Bitta lahza — ikki soat: Toshkentda 19:00, Moskvada 17:00. Kalendar timeZone: "Asia/Tashkent" bilan ko'rsatsa, Moskvadagi mehmon ham 19:00 ni ko'radi. en-GB — 24 soatli 19:00 formati uchun (Intl.DateTimeFormat). Biz faqat raqamlarni olyapmiz, oy nomi kerak emas.

6.2 Tuzoq: brauzer zonasi

Endi xato variant. Kalendar "19:00" ni mahalliy vaqt deb new Date("2026-10-20T19:00") qildi. Moskvadagi brauzer buni o'z zonasida tushunadi, ya'ni +03:00 bilan:

js
// ❌ Moskvadagi brauzer "19:00" ni o'z soati deb oldi
const wrong = new Date("2026-10-20T19:00:00+03:00");
console.log(wrong.toISOString()); // 2026-10-20T16:00:00.000Z
console.log(timeIn(wrong, "Asia/Tashkent")); // 21:00

Mehmon 19:00 ni tanladi, oshxona uni 21:00 da kutadi. Toshkentdagi dasturchi bu xatoni hech qachon ko'rmaydi — uning brauzeri ham UTC+5.

Davosi:

  • Slot vaqtini joy zonasi bilan lahzaga aylantiring: +05:00 yoki Temporal'dagi ZonedDateTime [Asia/Tashkent] bilan (Temporal). Yozgi vaqti bor zonalarda (Yevropa, AQSh) offsetni qo'lda yozib bo'lmaydi — Temporal yoki kutubxona kerak.
  • Faqat sana bo'lsa (ta'til, tug'ilgan kun) — "YYYY-MM-DD" satri, umuman lahza emas.
  • Kutubxonalarda zona sozlamasi bor: react-day-picker'da timeZone prop, FullCalendar'da timeZone opsiyasi. Sukut bo'yicha ular brauzer zonasida ishlaydi.

Tekshirib ko'ring: Server bronni 2026-10-20T14:00:00.000Z deb saqladi. Toshkentdagi Jasur aka va Moskvadagi mehmon bu bronni o'z ekranlarida timeIn(…, "Asia/Tashkent") bilan ko'rsa, nimani ko'radi?

Javob

Ikkalasi ham 19:00. Lahza bitta, zona ham bitta (Asia/Tashkent) — brauzerning zonasi ishtirok etmaydi. Shuning uchun bron ilovalari "restoran vaqti" ni alohida yozadi: "19:00 (Toshkent vaqti)".

7. Kutubxonalar

7.1 Ikki xil vazifa

Kalendar kutubxonalari ikki guruhga bo'linadi:

Vazifa Kutubxona Qayerda
Sana yoki oraliq tanlash (forma ichida) react-day-picker bron formasi, filtr, tug'ilgan kun
Voqealarni ko'rsatish va boshqarish (oy/hafta/kun) react-big-calendar, FullCalendar, Schedule-X admin panel, xodimlar jadvali, zal bandligi

«Bahor»ning mehmon sahifasiga birinchisi, Jasur akaning paneliga ikkinchisi kerak.

7.2 react-day-picker

shadcn/ui'ning Calendar komponenti aynan shu kutubxona ustida (Maxsus inputlar darsida ko'rgandik). Oraliq rejimi — bitta prop:

tsx
import { useState } from "react";
import { DayPicker } from "react-day-picker";
import type { DateRange } from "react-day-picker";
import "react-day-picker/style.css";

function VacationPicker({ booked }: { booked: Date[] }) {
  const [range, setRange] = useState<DateRange | undefined>();
  return (
    <DayPicker
      mode="range"
      selected={range}
      onSelect={setRange}
      disabled={[{ before: new Date() }, ...booked]}
      excludeDisabled // oraliqqa band kun tushsa — qaytadan
      min={1}
      max={14}
    />
  );
}

Bizning o'quv modelimizdagi 3-qoida (pickRange) — bu yerda excludeDisabled. min/max — oraliq uzunligi chegarasi. Klaviatura (strelkalar, PageUp/PageDown, Home/End) va ARIA — tayyor. timeZone va locale proplari bor.

Versiya haqida: kanon react-day-picker 10.0.2 ni o'lchadi. 10-versiya hujjati yangi loyihalarga paketning yangi nomini — @daypicker/react ni tavsiya qiladi. Eski nom moslik uchun ishlab turibdi. Eski maqolalarda esa lokal date-fns/locale dan olinadi (8-versiya) — 10 da u kerak emas.

7.3 react-big-calendar

Google Calendar uslubidagi oy/hafta/kun ko'rinishlari. Sanalarni hisoblash uchun "lokalizator" (date-fns, Day.js, Moment) talab qiladi — kanon date-fns 4.4.0 bilan o'lchadi:

tsx
import { Calendar, dateFnsLocalizer } from "react-big-calendar";
import { format, getDay, parse, startOfWeek } from "date-fns";
import "react-big-calendar/lib/css/react-big-calendar.css";

const localizer = dateFnsLocalizer({
  format, parse, startOfWeek, getDay, locales: {},
});

<Calendar
  localizer={localizer}
  events={bookings} // { title, start: Date, end: Date }
  defaultView="week"
  views={["month", "week", "day"]}
  step={30} // bitta katak — 30 daqiqa
  timeslots={2} // soatga 2 katak
  selectable
  onSelectSlot={({ start, end }) => offerBooking(start, end)}
/>;

selectable + onSelectSlot — bo'sh joyni sichqoncha bilan "chizib" bron taklif qilish. Bronni sudrab ko'chirish alohida qo'shimcha bilan ulanadi.

7.4 FullCalendar 7

Imkoniyatlari keng kalendar: oy, hafta, kun, ro'yxat ko'rinishlari. React, Vue, Angular va oddiy JS uchun bir xil API. 7-versiyada plaginlar @fullcalendar/react/… ichiga ko'chdi:

tsx
import FullCalendar from "@fullcalendar/react";
import timeGridPlugin from "@fullcalendar/react/timegrid";
import interactionPlugin from "@fullcalendar/react/interaction";
import "@fullcalendar/react/skeleton.css";

<FullCalendar
  plugins={[timeGridPlugin, interactionPlugin]}
  initialView="timeGridWeek"
  slotDuration="00:30:00"
  selectable // interaction plagini kerak
  select={(info) => console.log(info.startStr, info.endStr)}
  events={[{ title: "Bron: 4 kishi", start: "2026-10-20T19:00:00" }]}
/>;

Ikki narsa. select dagi end — eksklyuziv («Oxiri ichidami yoki tashqarisidami?» bo'limida aytdik). 7-versiya temporal-polyfill ni talab qiladi — sanalarni ichida Temporal bilan hisoblaydi. Eski (6-versiya) maqolalarda @fullcalendar/daygrid kabi alohida paketlarni ko'rasiz.

Kanon faqat oy ko'rinishini (dayGridMonth) o'lchadi. Yuqoridagi hafta ko'rinishi va select — hujjat bo'yicha, ishga tushirilmagan.

7.5 Schedule-X

Yangi avlod kalendari: zamonaviy dizayn, ko'rinishlar alohida funksiyalar. Kanon tajribasidagi kod:

tsx
import { ScheduleXCalendar, useCalendarApp } from "@schedule-x/react";
import { createViewMonthGrid } from "@schedule-x/calendar";

function Kalendar() {
  const calendar = useCalendarApp({
    views: [createViewMonthGrid()],
    events: [],
  });
  return <ScheduleXCalendar calendarApp={calendar} />;
}

U ham Temporal polyfill'iga tayanadi — va aynan shu joyda kutilmagan muammo chiqdi (keyingi bo'lim).

8. Biz sinab ko'rdik: hajm, CSP va bog'liqlik to'qnashuvi

8.1 Hajm

Kanon tajribasida (tajriba/kutubxonalar/) har kutubxonaning eng kichik ishlaydigan misoli Vite 8.3.3 bilan yig'ildi. Hajm — faqat React'dan ortig'i, gzip:

Kalendar kutubxonalari: JS (gzip, React'dan tashqari)
  • react-day-picker 10.0.2+1.6 KB CSS19,6 KB
  • Schedule-X 4.9.147,5 KB
  • FullCalendar 7.1.1+1.2 KB CSS56,5 KB
  • react-big-calendar 1.20.0 + date-fns+2.4 KB CSS58,6 KB

Manba: O'lchandi: kanon tajribasi, Vite 8.3.3, React 19.3.0, gzip -9, Chrome 155, natijalar/tajriba/kutubxonalar-21.txt, 2026-10-17

Taqqoslash uchun: React va react-dom'ning o'zi — 66.1 KB. Bitta sana tanlagich uchun +19.6 KB — sezilarli, lekin qabul qilsa bo'ladi. To'liq hafta jadvali esa React'ning deyarli yarmicha. Shuning uchun uni faqat kerakli sahifada, kech yuklash bilan ulash mantiqli.

8.2 CSP va Trusted Types

Har misol vazifalar ning CSP'si va Trusted Types ostida ochildi (Brauzer xavfsizlik modeli). Xom natijadan (kutubxonalar-21.txt):

text
kalendar-bigcalendar   JS  127686 (+  58.6 KB) CSS   2394 | CSP 0 | xato: yo'q | ochiq: tashqi yo'q
kalendar-daypicker     JS   87813 (+  19.6 KB) CSS   1623 | CSP 0 | xato: yo'q | ochiq: tashqi yo'q
kalendar-fullcalendar  JS  125530 (+  56.5 KB) CSS   1210 | CSP 0 | xato: yo'q | ochiq: tashqi yo'q
kalendar-schedulex     JS  116313 (+  47.5 KB) CSS      0 | CSP img-src×1 | xato: Loading the image 'data:image/svg+xml,%3c%3fxml version='1.0' encoding='utf-8'%3f%3e%3c!-- Uploaded  | ochiq: tashqi yo'q

Uch kutubxona — CSP 0: innerHTML yo'q, sahifaga <style> qo'shmaydi, tashqi serverga bormaydi. Schedule-X esa bitta buzilish berdi: o'z ikonkasini data: manzilli SVG rasm qilib chizadi, img-src esa data: ga ruxsat bermaydi. Kalendar ishlaydi, faqat ikonka ko'rinmaydi. Tuzatish — CSP'ga img-src 'self' data: qo'shish. data: rasmlar odatda xavfsiz hisoblanadi, lekin bu baribir siyosatni kengaytirish — ongli qaror bo'lsin.

8.3 Ikki kalendar bitta loyihada sig'madi

Kanonda to'rttasi bitta tajriba loyihasiga o'rnatildi va birinchi urinish to'xtadi (kutubxonalar-ornatish.txt):

text
# 1-urinish: ERESOLVE — temporal-polyfill: @fullcalendar/core 7.1.1 ^1.0.1, @schedule-x/calendar 4.9.1 0.3.2 (bitta loyihada ikkalasi bo'lmaydi) → --legacy-peer-deps

FullCalendar 7 temporal-polyfill ning ^1.0.1 versiyasini, Schedule-X 4.9.1 esa aynan 0.3.2 ni peer bog'liqlik qilib so'raydi (bog'liqlik turlari). Ikki talab bir-biriga zid. Tajribada --legacy-peer-deps bilan o'rnatildi va Schedule-X temporal-polyfill 1.0.5 bilan qayta o'lchandi (yuqoridagi raqam). Haqiqiy loyihada bu yo'l xavfli: Schedule-X o'zi sinalmagan polyfill bilan ishlaydi.

Saboq: Temporal hali hamma brauzerda yo'q (Temporal), kutubxonalar esa uni allaqachon polyfill bilan ishlatyapti — polyfill versiyalari to'qnashadi. Ikki kalendar kutubxonasini bitta loyihada ishlatmang.

8.4 Nega vazifalar da kalendar yo'q

Bu darsda vazifalar kodi o'zgarmaydi. Sabab oddiy: vazifada sana maydoni yo'q. 14-qismda "keyingi muddat" bo'yicha tartiblash qadami shu sababli qo'shilmagan edi. Kalendar faqat sana bor joyda ma'noga ega. Kanon kutubxonalarni alohida tajribada o'lchadi — keyin vazifalar ga muddat qo'shilsa, tanlov raqamlar bilan tayyor: forma uchun react-day-picker, CSP 0.

9. Hujumchi nigohi

Kalendar o'tgan kunlarni, band slotlarni va 22:00 dan keyingi vaqtni o'chirdi. Lekin bu — faqat interfeys. Hujumchi brauzerni chetlab, to'g'ridan-to'g'ri so'rov yuboradi:

js
fetch("/api/bron", {
  method: "POST",
  body: JSON.stringify({
    sana: "2026-10-21",
    vaqt: "03:00",
    kishi: 500,
  }),
});

Band kun, tungi soat, 500 kishi. Server tekshirmasa, bularning hammasi bazaga yoziladi. Yana bir hujum — ikki marta bron: ikki so'rov bir paytda kelsa, ikkalasi ham "bo'sh" ni ko'radi va ikkalasi yoziladi.

Himoya — faqat serverda:

  • Ish soati, o'tgan sana, kishi soni — server qoidalari bilan qayta tekshiriladi (klientdagi isDisabled — faqat qulaylik).
  • Bandlik bazada tranzaksiya yoki noyoblik cheklovi bilan tekshiriladi — ikkinchi so'rov "band bo'lib qoldi" oladi. Kalendar bu javobni chiroyli ko'rsatishi kerak (diagrammadagi oxirgi qadam).
  • Bir IP'dan yuzlab bron — so'rovlar chegarasi (rate limit).

Bular backend qismlarida (24-qismdan) to'liq yoziladi. Frontend dasturchining vazifasi — serverning rad javobini kutish va mehmonga tushunarli qilib aytish.

10. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Kalendar bir oy siljigan new Date(2026, 10, 1) — bu noyabr (oylar 0 dan) month ni 0 dan hisoblang, nomlarni massivdan oling
Ba'zi mehmonlarda sana bir kun oldinga ketgan new Date("2026-10-20") — UTC yarim tuni, mahalliy vaqtda kecha sanani "YYYY-MM-DD" satri qilib saqlang
Hafta yakshanbadan boshlanadi getDay() da 0 = yakshanba (day + 6) % 7 yoki kutubxonada weekStartsOn
19:00 band ko'rinadi, aslida bo'sh oraliq yopiq deb hisoblangan (<=) yarim ochiq: a < d && c < b
Klaviatura band kunda to'xtab qoladi band kun disabled aria-disabled + bosishni e'tiborsiz qoldirish
Moskvadagi mehmonning broni 2 soat siljigan slot brauzer zonasida lahzaga aylangan joy zonasi bilan: +05:00 yoki Temporal
npm i → ERESOLVE … temporal-polyfill ikki kalendar turli polyfill so'raydi bitta kalendar kutubxonasi
Loading the image 'data:image/svg+xml…' violates … img-src Schedule-X ikonkasi img-src 'self' data: — ongli qaror bilan

11. Mashqlar

Mashqlar kurs/mashqlar/21/01-kalendar-bron/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.

1-mashq (oson): bo'sh joylarni to'ldiring

  • new Date(Date.UTC(2026, 10, 1)) qaysi oyning 1-kuni:
  • getUTCDay() 0 qaytarsa, bu haftaning:
  • 18:00–20:00 va 20:00–22:00 bronlari yarim ochiq oraliqda kesishadimi (ha/yo'q):
  • react-day-picker'da oraliq tanlash: mode=""
  • Klaviatura band kunga ham borishi uchun disabled o'rniga:
Yechim

Oylar 0 dan, shuning uchun 10 — noyabr. Yakshanbani getUTCDay 0 deb sanaydi. Shu sababli haftani (day + 6) % 7 bilan dushanbadan boshlaymiz. Bronlarda esa 20:00 < 20:00 — false, demak ular kesishmaydi: bitta stol ketma-ket ikki mehmonga xizmat qiladi.

DayPicker'ning oraliq rejimi — mode="range". Band kunda aria-disabled tugma fokus oladi va ekran o'quvchi uni "band" deb o'qiydi, disabled esa fokusdan butunlay chiqarib tashlaydi.

2-mashq (o'rta): oyni almashtirish

«Oy setkasi» bo'limidagi monthWeeks ga «Oldingi oy» va «Keyingi oy» tugmalarini qo'shing. Sarlavha — noyabr 2026 kabi (oy nomlari — MONTHS massividan). Dekabrdan keyin yanvar va yil +1 bo'lsin.

Ishora: oy va yilni bitta son qilib hisoblash qulay — year * 12 + month. Undan yilni Math.floor(n / 12) bilan, oyni n % 12 bilan qaytarasiz (arifmetik operatorlar).

Sinov: oktabr 2026 dan uch marta «Keyingi oy» bosilsa, konsolga sarlavha va birinchi hafta chiqsin.

Yechim
jsx
import { useEffect, useState } from "react";

const MONTHS = ["yanvar", "fevral", "mart", "aprel", "may", "iyun",
  "iyul", "avgust", "sentabr", "oktabr", "noyabr", "dekabr"];

function monthWeeks(year, month) {
  const first = new Date(Date.UTC(year, month, 1)).getUTCDay();
  const days = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
  const cells = Array((first + 6) % 7).fill(null);
  for (let d = 1; d <= days; d++) cells.push(d);
  while (cells.length % 7 !== 0) cells.push(null);
  const weeks = [];
  for (let i = 0; i < cells.length; i += 7) {
    weeks.push(cells.slice(i, i + 7));
  }
  return weeks;
}

export default function App() {
  // bitta son: yil * 12 + oy
  const [index, setIndex] = useState(2026 * 12 + 9);
  const year = Math.floor(index / 12);
  const month = index % 12;
  const title = `${MONTHS[month]} ${year}`;
  const weeks = monthWeeks(year, month);
  useEffect(() => {
    console.log(title, weeks[0].join(" "));
  }, [title, weeks]);
  return (
    <section>
      <button type="button" onClick={() => setIndex((i) => i - 1)}>
        Oldingi oy
      </button>
      <h2>{title}</h2>
      <button type="button" onClick={() => setIndex((i) => i + 1)}>
        Keyingi oy
      </button>
    </section>
  );
}

Konsolda:

text
oktabr 2026    1 2 3 4
noyabr 2026       1
dekabr 2026  1 2 3 4 5 6
yanvar 2027     1 2 3

Massivdagi null lar join(" ") da bo'sh satrga aylanadi. Shuning uchun qator boshida bo'sh joylar bor: 1-yanvar 2027 — juma, oldida to'rtta bo'sh katak.

Hisoblagich index bitta son bo'lgani uchun yil almashishini alohida tekshirish shart emas: 2026 × 12 + 11 + 1 = 2027 × 12 + 0. Effekt ham title o'zgargandagina ishlaydi.

3-mashq (qiyin): stol kishi soniga qarab

«Vaqt slotlari» bo'limidagi hisobni kengaytiring. Stollar har xil:

js
const TABLES = [
  { id: 1, seats: 2 },
  { id: 2, seats: 4 },
  { id: 3, seats: 6 },
];

findTable(bookings, start, guests) funksiyasini yozing: shu vaqtdan 2 soatga bo'sh va seats >= guests bo'lgan stollardan eng kichigini qaytarsin (yo'q bo'lsa — null). Bronlar — darsdagi BOOKINGS (table — stol id si). Keyin 4 kishilik oila uchun 17:00 dan 20:00 gacha har slotni konsolga chiqaring: 17:00 → 3-stol yoki 17:30 → bo'sh stol yo'q.

Ishora: avval bo'sh stollarni filter, keyin toSorted((a, b) => a.seats - b.seats) (nusxa bilan o'zgartirish) va birinchisi.

Yechim
js
const toMin = (hhmm) => {
  const [h, m] = hhmm.split(":").map(Number);
  return h * 60 + m;
};
const toHhmm = (min) =>
  `${String(Math.floor(min / 60)).padStart(2, "0")}:` +
  String(min % 60).padStart(2, "0");

const LENGTH = 120;
const TABLES = [
  { id: 1, seats: 2 },
  { id: 2, seats: 4 },
  { id: 3, seats: 6 },
];
const BOOKINGS = [
  { table: 1, start: "18:00", end: "20:00" },
  { table: 1, start: "20:00", end: "22:00" },
  { table: 2, start: "19:00", end: "21:00" },
  { table: 3, start: "17:00", end: "19:00" },
];

const overlaps = (a, b, c, d) => a < d && c < b;

function findTable(bookings, start, guests) {
  const end = start + LENGTH;
  const free = TABLES.filter(
    (t) =>
      t.seats >= guests &&
      !bookings.some(
        (b) =>
          b.table === t.id &&
          overlaps(start, end, toMin(b.start), toMin(b.end)),
      ),
  );
  return free.toSorted((a, b) => a.seats - b.seats)[0] ?? null;
}

for (let t = toMin("17:00"); t <= toMin("20:00"); t += 30) {
  const table = findTable(BOOKINGS, t, 4);
  const text = table ? `${table.id}-stol` : "bo'sh stol yo'q";
  console.log(`${toHhmm(t)} → ${text}`);
}

Konsolda:

text
17:00 → 2-stol
17:30 → bo'sh stol yo'q
18:00 → bo'sh stol yo'q
18:30 → bo'sh stol yo'q
19:00 → 3-stol
19:30 → 3-stol
20:00 → 3-stol

17:00 da 2-stol (4 o'rin) va 3-stol (6 o'rin) emas — 2-stol band emas edi va u kichikroq. 1-stol 2 o'rinli, 4 kishiga sig'maydi. 19:00 dan 2-stol band (19:00–21:00), shuning uchun 6 o'rinli 3-stol. Eng kichik mos stol — restoran o'rinlarini tejash qoidasi: katta stol katta oilaga qolsin.

4-mashq: tajriba qadami (ixtiyoriy)

vazifalar ga tegmang. Alohida papkada (kurs/tajriba/kalendar/) Vite + React loyiha oching va react-day-picker@10.0.2 bilan Sardorning ta'til formasini yasang: mode="range", band kunlar, excludeDisabled, max={14}. Keyin ikki narsani tekshiring: npm run build dagi gzip hajmi va index.html ga vazifalar dagi CSP <meta> i qo'shilganda konsoldagi xatolar.

Yechim

Tekshiruv ro'yxati:

  • O'rnatish: npm i -E react-day-picker@10.0.2 (yoki yangi nomi @daypicker/react) — date-fns alohida kerak emas.
  • Band kun bosilmaydi; oraliqqa band kun tushsa, tanlov qaytadan boshlanadi; 14 kundan uzun oraliq tanlanmaydi.
  • Strelkalar, PageUp/PageDown ishlaydi; Tab kalendarga bitta to'xtash.
  • Build: Vite oxirida har faylning gzip hajmini chiqaradi. Kanonda faqat React'dan ortig'i — 19.6 KB JS va 1.6 KB CSS. Sizning raqamingiz forma va boshqa kodga qarab biroz farq qiladi.
  • CSP bilan: konsol toza bo'lishi kerak (kanonda — 0 buzilish).

12. Real ishda

  • Bron xizmatlari (restoran, salon, klinika, mehmonxona) — slot hisobi va bandlik serverda, frontend faqat bo'sh vaqtlarni so'raydi. Ko'p xizmatlarda slot ro'yxati API'dan tayyor keladi.
  • Admin panellar va CRM — xodimlar jadvali, zal bandligi: react-big-calendar yoki FullCalendar. FullCalendar'ning resurslar (xona, stol, xodim) bo'yicha ko'rinishlari — pullik Premium litsenziyada (fullcalendar.io/pricing).
  • Formalar — sana yoki oraliq tanlagich: react-day-picker, shadcn/ui Calendar. Ko'p jamoalar oddiy <input type="date"> bilan boshlaydi va faqat band kunlar kerak bo'lganda kutubxonaga o'tadi.
  • Intervyu savollari: "Ikki bron kesishishini qanday tekshirasiz?", "Bron vaqtini qaysi zonada saqlaysiz?", "Kalendar klaviaturada qanday ishlashi kerak?", "end eksklyuziv nima degani?".

Xulosa

  • Oy setkasi: kunlar soni (Date.UTC(y, m + 1, 0)), 1-kunning hafta kuni ((day + 6) % 7), 7 ustun; sana state'da "YYYY-MM-DD" satri.
  • Kalendar klaviaturada — roving tabindex: bitta Tab to'xtashi, strelkalar, band kun aria-disabled.
  • Oraliq: ikki bosish, band kun tushsa — qaytadan; ta'til — yopiq oraliq, mehmonxona va slotlar — yarim ochiq [boshi, oxiri).
  • Vaqt sloti bo'sh — agar stolning birorta broni u bilan kesishmasa: a < d && c < b.
  • Vaqt joy zonasida ko'rsatiladi, serverga lahza bo'lib boradi; brauzer zonasiga ishonmang.
  • Kutubxonalar: tanlash — react-day-picker (+19.6 KB), jadval — react-big-calendar (+58.6), FullCalendar (+56.5), Schedule-X (+47.5, CSP'da data: ikonka); ikki Temporal'li kalendar bitta loyihada sig'madi.

Keyingi dars: i18n I: react-i18next bilan ko'p tilli ilova — «Bahor» va vazifalar ruscha gapira boshlaydi: matnlar tarjima fayllariga chiqadi, ko'plik shakllari ("2 задачи", "5 задач") kutubxona hisobida.

Manbalar

  • React DayPicker 10: "Range mode", "Disabling dates", "Setting time zone", "Upgrading" — daypicker.dev
  • react-big-calendar: "Calendar props" (selectable, onSelectSlot, step, timeslots) — jquense.github.io/react-big-calendar
  • FullCalendar 7: "Upgrading from v6", "selectable", "select callback" — fullcalendar.io/docs; litsenziya — fullcalendar.io/pricing
  • Schedule-X: "React" — schedule-x.dev
  • WAI-ARIA APG: "Date Picker Dialog Example", "Grid pattern" — w3.org/WAI/ARIA/apg
  • MDN: Date.UTC(), Intl.DateTimeFormat (timeZone) — developer.mozilla.org
  • Kanon: natijalar/tajriba/kutubxonalar-21.txt, kutubxonalar-ornatish.txt, tajriba/kutubxonalar/src/k/kalendar-*.tsx (2026-10-17)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!