3 Commits
v2.1.2 ... main

Author SHA1 Message Date
0ee77ee5c1 chore(release): v2.1.3
All checks were successful
CI / lint-and-test (push) Successful in 1m7s
Docker Build and Release / build-and-push (push) Successful in 1m30s
Docker Build and Release / release (push) Successful in 9s
Made-with: Cursor
2026-03-07 00:40:47 +03:00
3f34c7951f feat: enhance CalendarDay component with holiday outline styling
All checks were successful
CI / lint-and-test (push) Successful in 1m4s
- Added a new CSS variable for today's holiday outline to improve visual distinction on the calendar.
- Updated the CalendarDay component to apply the holiday outline styling when the day is marked as today and has associated event summaries.
- Enhanced unit tests to verify the correct application of styles for today and holiday events, ensuring proper rendering and user experience.
2026-03-07 00:35:26 +03:00
dc87b3ad97 feat: update copy functionality in ContactLinks component
- Modified the copy button behavior to display "Copied" via button aria-label instead of a tooltip after a successful copy action.
- Enhanced the component to revert the copy button icon when switching between phone number and Telegram username copy actions.
- Updated tests to reflect changes in copy button functionality and ensure proper rendering based on user interactions.
2026-03-07 00:11:36 +03:00
7 changed files with 95 additions and 61 deletions

View File

@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [2.1.3] - 2025-03-07
(No changes documented; release for version sync.)
## [2.1.2] - 2025-03-06
(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.
- 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.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 -->

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "duty-teller"
version = "2.1.2"
version = "2.1.3"
description = "Telegram bot for team duty shift calendar and group reminder"
readme = "README.md"
requires-python = ">=3.11"

View File

@@ -76,6 +76,7 @@
--today-hover: color-mix(in srgb, var(--bg) 15%, var(--today));
--today-border: color-mix(in srgb, var(--today) 35%, 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);
--muted-fade: color-mix(in srgb, var(--muted) 40%, transparent);
--handle-bg: color-mix(in srgb, var(--muted) 80%, var(--text));

View File

@@ -76,4 +76,20 @@ describe("CalendarDay", () => {
const button = screen.getByRole("button", { name: /15/ });
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");
});
});

View File

@@ -45,6 +45,7 @@ function CalendarDayInner({
[duties]
);
const hasEvent = eventSummaries.length > 0;
const isTodayHoliday = isToday && hasEvent;
const showIndicator = !isOtherMonth;
const hasAny = duties.length > 0 || hasEvent;
@@ -82,10 +83,9 @@ function CalendarDayInner({
showIndicator && hasAny && "font-bold",
showIndicator &&
hasEvent &&
!isToday &&
"bg-[linear-gradient(135deg,var(--surface)_0%,var(--today-gradient-end)_100%)] border border-[var(--today-border)]",
isToday &&
hasEvent &&
"bg-today text-[var(--bg)] border border-[var(--today-border-selected)]"
isTodayHoliday && "ring-1 ring-inset ring-[var(--today-holiday-outline)]"
)}
onClick={(e) => {
if (isOtherMonth) return;

View File

@@ -184,9 +184,9 @@ describe("ContactLinks", () => {
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);
renderWithTooltip(
render(
<ContactLinks
phone="+79991234567"
username={null}
@@ -201,10 +201,44 @@ describe("ContactLinks", () => {
fireEvent.click(copyBtn);
});
const tooltip = await screen.findByRole("tooltip", {
name: /Copied|Скопировано/i,
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
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", () => {

View File

@@ -12,14 +12,8 @@ import { openPhoneLink } from "@/lib/open-phone-link";
import { openTelegramProfile } from "@/lib/telegram-link";
import { triggerHapticLight } from "@/lib/telegram-haptic";
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 { 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;
@@ -128,30 +122,22 @@ export function ContactLinks({
<span className="truncate">{formatPhoneDisplay(phone!)}</span>
</a>
{showCopy && (
<Tooltip
open={copiedKind === "phone"}
onOpenChange={(open) => {
if (!open) setCopiedKind(null);
<button
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]"
aria-label={copiedKind === "phone" ? t("contact.copied") : t("contact.copy_phone")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
void handleCopyPhone(e);
}}
>
<TooltipTrigger asChild>
<button
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]"
aria-label={t("contact.copy_phone")}
onClick={(e) => {
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>
{copiedKind === "phone" ? (
<Check className="size-5" aria-hidden />
) : (
<Copy className="size-5" aria-hidden />
)}
</button>
)}
</div>
)}
@@ -173,30 +159,22 @@ export function ContactLinks({
<span className="truncate">@{cleanUsername}</span>
</a>
{showCopy && (
<Tooltip
open={copiedKind === "telegram"}
onOpenChange={(open) => {
if (!open) setCopiedKind(null);
<button
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]"
aria-label={copiedKind === "telegram" ? t("contact.copied") : t("contact.copy_telegram")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
void handleCopyTelegram(e);
}}
>
<TooltipTrigger asChild>
<button
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]"
aria-label={t("contact.copy_telegram")}
onClick={(e) => {
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>
{copiedKind === "telegram" ? (
<Check className="size-5" aria-hidden />
) : (
<Copy className="size-5" aria-hidden />
)}
</button>
)}
</div>
)}