Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ee77ee5c1 | |||
| 3f34c7951f | |||
| dc87b3ad97 |
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.1.3] - 2025-03-07
|
||||||
|
|
||||||
|
(No changes documented; release for version sync.)
|
||||||
|
|
||||||
## [2.1.2] - 2025-03-06
|
## [2.1.2] - 2025-03-06
|
||||||
|
|
||||||
(No changes documented; release for version sync.)
|
(No changes documented; release for version sync.)
|
||||||
@@ -68,7 +72,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Input validation and initData hash verification for Miniapp access.
|
- Input validation and initData hash verification for Miniapp access.
|
||||||
- Optional CORS and init_data_max_age; use env for secrets.
|
- Optional CORS and init_data_max_age; use env for secrets.
|
||||||
|
|
||||||
[Unreleased]: https://github.com/your-org/duty-teller/compare/v2.1.2...HEAD
|
[Unreleased]: https://github.com/your-org/duty-teller/compare/v2.1.3...HEAD
|
||||||
|
[2.1.3]: https://github.com/your-org/duty-teller/releases/tag/v2.1.3
|
||||||
[2.1.2]: https://github.com/your-org/duty-teller/releases/tag/v2.1.2
|
[2.1.2]: https://github.com/your-org/duty-teller/releases/tag/v2.1.2
|
||||||
[2.1.1]: https://github.com/your-org/duty-teller/releases/tag/v2.1.1 <!-- placeholder: set to your repo URL when publishing -->
|
[2.1.1]: https://github.com/your-org/duty-teller/releases/tag/v2.1.1 <!-- placeholder: set to your repo URL when publishing -->
|
||||||
[2.0.6]: https://github.com/your-org/duty-teller/releases/tag/v2.0.6 <!-- placeholder: set to your repo URL when publishing -->
|
[2.0.6]: https://github.com/your-org/duty-teller/releases/tag/v2.0.6 <!-- placeholder: set to your repo URL when publishing -->
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "duty-teller"
|
name = "duty-teller"
|
||||||
version = "2.1.2"
|
version = "2.1.3"
|
||||||
description = "Telegram bot for team duty shift calendar and group reminder"
|
description = "Telegram bot for team duty shift calendar and group reminder"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
--today-hover: color-mix(in srgb, var(--bg) 15%, var(--today));
|
--today-hover: color-mix(in srgb, var(--bg) 15%, var(--today));
|
||||||
--today-border: color-mix(in srgb, var(--today) 35%, transparent);
|
--today-border: color-mix(in srgb, var(--today) 35%, transparent);
|
||||||
--today-border-selected: color-mix(in srgb, var(--bg) 50%, transparent);
|
--today-border-selected: color-mix(in srgb, var(--bg) 50%, transparent);
|
||||||
|
--today-holiday-outline: color-mix(in srgb, var(--bg) 28%, var(--today));
|
||||||
--today-gradient-end: color-mix(in srgb, var(--today) 15%, transparent);
|
--today-gradient-end: color-mix(in srgb, var(--today) 15%, transparent);
|
||||||
--muted-fade: color-mix(in srgb, var(--muted) 40%, transparent);
|
--muted-fade: color-mix(in srgb, var(--muted) 40%, transparent);
|
||||||
--handle-bg: color-mix(in srgb, var(--muted) 80%, var(--text));
|
--handle-bg: color-mix(in srgb, var(--muted) 80%, var(--text));
|
||||||
|
|||||||
@@ -76,4 +76,20 @@ describe("CalendarDay", () => {
|
|||||||
const button = screen.getByRole("button", { name: /15/ });
|
const button = screen.getByRole("button", { name: /15/ });
|
||||||
expect(button.getAttribute("aria-disabled")).not.toBe("true");
|
expect(button.getAttribute("aria-disabled")).not.toBe("true");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("applies today base and holiday outline when isToday and eventSummaries are set", () => {
|
||||||
|
render(
|
||||||
|
<CalendarDay
|
||||||
|
{...defaultProps}
|
||||||
|
isOtherMonth={false}
|
||||||
|
isToday={true}
|
||||||
|
eventSummaries={["Holiday"]}
|
||||||
|
onDayClick={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const button = screen.getByRole("button", { name: /15/ });
|
||||||
|
expect(button.className).toMatch(/bg-today|today/);
|
||||||
|
expect(button.className).toMatch(/ring-2|today-holiday-outline/);
|
||||||
|
expect(button.getAttribute("aria-disabled")).not.toBe("true");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ function CalendarDayInner({
|
|||||||
[duties]
|
[duties]
|
||||||
);
|
);
|
||||||
const hasEvent = eventSummaries.length > 0;
|
const hasEvent = eventSummaries.length > 0;
|
||||||
|
const isTodayHoliday = isToday && hasEvent;
|
||||||
const showIndicator = !isOtherMonth;
|
const showIndicator = !isOtherMonth;
|
||||||
const hasAny = duties.length > 0 || hasEvent;
|
const hasAny = duties.length > 0 || hasEvent;
|
||||||
|
|
||||||
@@ -82,10 +83,9 @@ function CalendarDayInner({
|
|||||||
showIndicator && hasAny && "font-bold",
|
showIndicator && hasAny && "font-bold",
|
||||||
showIndicator &&
|
showIndicator &&
|
||||||
hasEvent &&
|
hasEvent &&
|
||||||
|
!isToday &&
|
||||||
"bg-[linear-gradient(135deg,var(--surface)_0%,var(--today-gradient-end)_100%)] border border-[var(--today-border)]",
|
"bg-[linear-gradient(135deg,var(--surface)_0%,var(--today-gradient-end)_100%)] border border-[var(--today-border)]",
|
||||||
isToday &&
|
isTodayHoliday && "ring-1 ring-inset ring-[var(--today-holiday-outline)]"
|
||||||
hasEvent &&
|
|
||||||
"bg-today text-[var(--bg)] border border-[var(--today-border-selected)]"
|
|
||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (isOtherMonth) return;
|
if (isOtherMonth) return;
|
||||||
|
|||||||
@@ -184,9 +184,9 @@ describe("ContactLinks", () => {
|
|||||||
expect(triggerHapticLightMock).toHaveBeenCalled();
|
expect(triggerHapticLightMock).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows Copied text in tooltip after successful copy", async () => {
|
it("shows Copied via button aria-label and no tooltip after successful copy", async () => {
|
||||||
copyToClipboardMock.mockResolvedValue(true);
|
copyToClipboardMock.mockResolvedValue(true);
|
||||||
renderWithTooltip(
|
render(
|
||||||
<ContactLinks
|
<ContactLinks
|
||||||
phone="+79991234567"
|
phone="+79991234567"
|
||||||
username={null}
|
username={null}
|
||||||
@@ -201,10 +201,44 @@ describe("ContactLinks", () => {
|
|||||||
fireEvent.click(copyBtn);
|
fireEvent.click(copyBtn);
|
||||||
});
|
});
|
||||||
|
|
||||||
const tooltip = await screen.findByRole("tooltip", {
|
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
|
||||||
name: /Copied|Скопировано/i,
|
expect(
|
||||||
|
screen.getByRole("button", { name: /Copied|Скопировано/i })
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reverts first copy button to Copy icon when copying the other field", async () => {
|
||||||
|
copyToClipboardMock.mockResolvedValue(true);
|
||||||
|
render(
|
||||||
|
<ContactLinks
|
||||||
|
phone="+79991234567"
|
||||||
|
username="alice_dev"
|
||||||
|
layout="block"
|
||||||
|
showCopyButtons
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const copyPhoneBtn = screen.getByRole("button", {
|
||||||
|
name: /Copy phone number|Скопировать номер/i,
|
||||||
});
|
});
|
||||||
expect(tooltip).toBeInTheDocument();
|
const copyTelegramBtn = screen.getByRole("button", {
|
||||||
|
name: /Copy Telegram username|Скопировать логин Telegram/i,
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(copyPhoneBtn);
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: /Copied|Скопировано/i })
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(copyTelegramBtn);
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: /Copy phone number|Скопировать номер/i })
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: /Copied|Скопировано/i })
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not show copy buttons when showCopyButtons is false", () => {
|
it("does not show copy buttons when showCopyButtons is false", () => {
|
||||||
|
|||||||
@@ -12,14 +12,8 @@ import { openPhoneLink } from "@/lib/open-phone-link";
|
|||||||
import { openTelegramProfile } from "@/lib/telegram-link";
|
import { openTelegramProfile } from "@/lib/telegram-link";
|
||||||
import { triggerHapticLight } from "@/lib/telegram-haptic";
|
import { triggerHapticLight } from "@/lib/telegram-haptic";
|
||||||
import { copyToClipboard } from "@/lib/copy-to-clipboard";
|
import { copyToClipboard } from "@/lib/copy-to-clipboard";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Phone as PhoneIcon, Send as TelegramIcon, Copy } from "lucide-react";
|
import { Phone as PhoneIcon, Send as TelegramIcon, Copy, Check } from "lucide-react";
|
||||||
|
|
||||||
const COPIED_RESET_MS = 1800;
|
const COPIED_RESET_MS = 1800;
|
||||||
|
|
||||||
@@ -128,30 +122,22 @@ export function ContactLinks({
|
|||||||
<span className="truncate">{formatPhoneDisplay(phone!)}</span>
|
<span className="truncate">{formatPhoneDisplay(phone!)}</span>
|
||||||
</a>
|
</a>
|
||||||
{showCopy && (
|
{showCopy && (
|
||||||
<Tooltip
|
<button
|
||||||
open={copiedKind === "phone"}
|
type="button"
|
||||||
onOpenChange={(open) => {
|
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-r-[calc(theme(borderRadius.md)-1px)] text-accent hover:bg-accent/15 focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-[-2px]"
|
||||||
if (!open) setCopiedKind(null);
|
aria-label={copiedKind === "phone" ? t("contact.copied") : t("contact.copy_phone")}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
void handleCopyPhone(e);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TooltipTrigger asChild>
|
{copiedKind === "phone" ? (
|
||||||
<button
|
<Check className="size-5" aria-hidden />
|
||||||
type="button"
|
) : (
|
||||||
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-r-[calc(theme(borderRadius.md)-1px)] text-accent hover:bg-accent/15 focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-[-2px]"
|
<Copy className="size-5" aria-hidden />
|
||||||
aria-label={t("contact.copy_phone")}
|
)}
|
||||||
onClick={(e) => {
|
</button>
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
void handleCopyPhone(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Copy className="size-5" aria-hidden />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top" sideOffset={8}>
|
|
||||||
{t("contact.copied")}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -173,30 +159,22 @@ export function ContactLinks({
|
|||||||
<span className="truncate">@{cleanUsername}</span>
|
<span className="truncate">@{cleanUsername}</span>
|
||||||
</a>
|
</a>
|
||||||
{showCopy && (
|
{showCopy && (
|
||||||
<Tooltip
|
<button
|
||||||
open={copiedKind === "telegram"}
|
type="button"
|
||||||
onOpenChange={(open) => {
|
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-r-[calc(theme(borderRadius.md)-1px)] text-accent hover:bg-accent/15 focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-[-2px]"
|
||||||
if (!open) setCopiedKind(null);
|
aria-label={copiedKind === "telegram" ? t("contact.copied") : t("contact.copy_telegram")}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
void handleCopyTelegram(e);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TooltipTrigger asChild>
|
{copiedKind === "telegram" ? (
|
||||||
<button
|
<Check className="size-5" aria-hidden />
|
||||||
type="button"
|
) : (
|
||||||
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-r-[calc(theme(borderRadius.md)-1px)] text-accent hover:bg-accent/15 focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-[-2px]"
|
<Copy className="size-5" aria-hidden />
|
||||||
aria-label={t("contact.copy_telegram")}
|
)}
|
||||||
onClick={(e) => {
|
</button>
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
void handleCopyTelegram(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Copy className="size-5" aria-hidden />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top" sideOffset={8}>
|
|
||||||
{t("contact.copied")}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user