-
Notifications
You must be signed in to change notification settings - Fork 43
feat(web): productivity tools - copy citation button and print styles #157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
dc8f81e
b07416e
63ad0ad
7d16915
9c41df9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -676,11 +676,21 @@ main.narrow { | |
| /* Source CTA — solid, icon-led deep link to the procedure's documents on the public ЦАИС ЕОП | ||
| portal. Rendered as a child of the page header so it sits under the contract lede and is the | ||
| first action a reader sees. Solid ink → accent on hover, matching the site's button language. */ | ||
| .source-cta { | ||
| display: inline-flex; | ||
| .header-actions { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: var(--s-3); | ||
| margin-top: var(--s-6); | ||
| flex-wrap: wrap; | ||
| } | ||
|
|
||
| /* Source CTA & Save Button — solid, icon-led actions. | ||
| Solid ink → accent on hover, matching the site's button language. */ | ||
| .source-cta, | ||
| .save-btn { | ||
| display: inline-flex; | ||
| align-items: center; | ||
| gap: var(--s-3); | ||
| padding: 11px var(--s-5); | ||
| border: 1px solid var(--ink); | ||
| background: var(--ink); | ||
|
|
@@ -692,21 +702,28 @@ main.narrow { | |
| transition: | ||
| background 0.15s ease, | ||
| border-color 0.15s ease; | ||
| cursor: pointer; | ||
| } | ||
| /* Keep the label/icon legible once the link is visited — the global `a:visited { color: --ink }` | ||
| (specificity 0,1,1) would otherwise repaint the text ink-on-ink into an invisible black box. */ | ||
| .source-cta:visited { | ||
| color: var(--paper); | ||
| } | ||
| .source-cta:hover { | ||
| .source-cta:hover, | ||
| .save-btn:hover { | ||
| background: var(--accent); | ||
| border-color: var(--accent); | ||
| color: var(--paper); | ||
| text-decoration: none; | ||
| } | ||
| .source-cta svg { | ||
| .source-cta svg, | ||
| .save-btn svg { | ||
| flex: none; | ||
| } | ||
| .save-btn.is-saved { | ||
| background: var(--pos); | ||
| border-color: var(--pos); | ||
| } | ||
| .source-cta .cta-ext { | ||
| font-size: 13px; | ||
| opacity: 0.75; | ||
|
|
@@ -3548,3 +3565,49 @@ tbody td, | |
| .external-eik-link svg { | ||
| opacity: 0.7; | ||
| } | ||
|
|
||
| /* =========== Print Styles =========== */ | ||
| @media print { | ||
| /* Hide navigation, footers, search drawer, and actions */ | ||
| .site-header, | ||
| .site-nav, | ||
| .nav-backdrop, | ||
| .search-drawer, | ||
| .site-footer, | ||
| .save-btn { | ||
| display: none !important; | ||
| } | ||
|
|
||
| /* Expose URLs in print */ | ||
| a[href]:not([href^='#'])::after { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| content: ' (' attr(href) ')'; | ||
| word-break: break-all; | ||
| font-size: 0.9em; | ||
| } | ||
|
|
||
| /* Ensure background and text colors are optimized for printing */ | ||
| body { | ||
| background: #fff !important; | ||
| color: #000 !important; | ||
| } | ||
|
|
||
| /* Prevent elements from breaking mid-page */ | ||
| .section, | ||
| .table-wrap, | ||
| .result, | ||
| .facts { | ||
| break-inside: avoid; | ||
| } | ||
|
|
||
| /* Clean up page margins */ | ||
| @page { | ||
| margin: 1.5cm; | ||
| } | ||
|
|
||
| /* Expand the content to fill the page width */ | ||
| #main, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Селекторът |
||
| .page-header { | ||
| max-width: none !important; | ||
| padding: 0 !important; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import { useState, useCallback, useEffect, useRef } from 'react'; | ||
|
|
||
| export function CopyCitationButton({ textToCopy }: { textToCopy: string }) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Липсват тестове за този компонент. |
||
| const [copied, setCopied] = useState(false); | ||
| const timeoutRef = useRef<number | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| return () => { | ||
| if (timeoutRef.current !== null) { | ||
| window.clearTimeout(timeoutRef.current); | ||
| } | ||
| }; | ||
| }, []); | ||
|
|
||
| const handleCopy = useCallback(() => { | ||
| if (typeof navigator !== 'undefined' && navigator.clipboard) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. При липса на |
||
| navigator.clipboard | ||
| .writeText(textToCopy) | ||
| .then(() => { | ||
| setCopied(true); | ||
| if (timeoutRef.current !== null) { | ||
| window.clearTimeout(timeoutRef.current); | ||
| } | ||
| timeoutRef.current = window.setTimeout(() => setCopied(false), 2000); | ||
| }) | ||
| .catch((err) => { | ||
| console.error('Failed to copy text:', err); | ||
| }); | ||
| } | ||
| }, [textToCopy]); | ||
|
|
||
| return ( | ||
| <button | ||
| type="button" | ||
| onClick={handleCopy} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Достъпност: Бутонът има статичен |
||
| className={`save-btn ${copied ? 'is-saved' : ''}`} | ||
| aria-label="Копирай данните като цитат" | ||
| title="Копирай основните факти" | ||
| > | ||
| {copied ? ( | ||
| <svg | ||
| width="16" | ||
| height="16" | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| stroke="currentColor" | ||
| strokeWidth="2" | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| aria-hidden="true" | ||
| > | ||
| <polyline points="20 6 9 17 4 12"></polyline> | ||
| </svg> | ||
| ) : ( | ||
| <svg | ||
| width="16" | ||
| height="16" | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| stroke="currentColor" | ||
| strokeWidth="2" | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| aria-hidden="true" | ||
| > | ||
| <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> | ||
| <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> | ||
| </svg> | ||
| )} | ||
| <span className="save-btn-text" aria-live="polite"> | ||
| {copied ? 'Копирано!' : 'Копирай'} | ||
| </span> | ||
| </button> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| import { describe, it, expect } from 'vitest'; | ||
| import { buildContractCitation, buildCompanyCitation, buildAuthorityCitation } from './citation'; | ||
| import { money } from '@sigma/shared'; | ||
|
|
||
| describe('citation builders', () => { | ||
| it('builds a contract citation', () => { | ||
| const c = { | ||
| subject: 'Доставка на компютри', | ||
| authority: { name: 'Община Пловдив' }, | ||
| bidder: { displayName: 'Техно ООД' }, | ||
| value: { currentEur: 125000.5 }, | ||
| id: 'abc-123', | ||
| }; | ||
|
|
||
| const citation = buildContractCitation(c, 'https://sigma.test'); | ||
| expect(citation).toBe( | ||
| [ | ||
| 'Договор: Доставка на компютри', | ||
| 'Възложител: Община Пловдив', | ||
| 'Изпълнител: Техно ООД', | ||
| `Стойност: ${money(125000.5)}`, | ||
| 'Връзка: https://sigma.test/contracts/abc-123', | ||
| ].join('\n'), | ||
| ); | ||
| }); | ||
|
|
||
| it('handles contract with null value', () => { | ||
| const c = { | ||
| subject: 'Одит', | ||
| authority: { name: 'Община Пловдив' }, | ||
| bidder: { displayName: 'Техно ООД' }, | ||
| value: { currentEur: null }, | ||
| id: 'abc-123', | ||
| }; | ||
|
|
||
| const citation = buildContractCitation(c, 'https://sigma.test'); | ||
| expect(citation).toBe( | ||
| [ | ||
| 'Договор: Одит', | ||
| 'Възложител: Община Пловдив', | ||
| 'Изпълнител: Техно ООД', | ||
| 'Стойност: —', | ||
| 'Връзка: https://sigma.test/contracts/abc-123', | ||
| ].join('\n'), | ||
| ); | ||
| }); | ||
|
|
||
| it('builds a company citation with EIK', () => { | ||
| const c = { | ||
| displayName: 'Техно ООД', | ||
| eik: '123456789', | ||
| wonEur: 5000000, | ||
| contracts: 42, | ||
| slug: 'techno-ood', | ||
| hasEik: true, | ||
| }; | ||
|
|
||
| const citation = buildCompanyCitation(c, 'https://sigma.test'); | ||
| expect(citation).toBe( | ||
| [ | ||
| 'Компания: Техно ООД', | ||
| 'ЕИК: 123456789', | ||
| `Общо спечелено: ${money(5000000)}`, | ||
| 'Брой договори: 42', | ||
| 'Връзка: https://sigma.test/companies/techno-ood', | ||
| ].join('\n'), | ||
| ); | ||
| }); | ||
|
|
||
| it('builds a company citation without EIK', () => { | ||
| const c = { | ||
| displayName: 'Чуждестранна фирма', | ||
| eik: null, | ||
| wonEur: 0, | ||
| contracts: 1, | ||
| slug: 'foreign-corp', | ||
| hasEik: false, | ||
| }; | ||
|
|
||
| const citation = buildCompanyCitation(c, 'https://sigma.test'); | ||
| expect(citation).toBe( | ||
| [ | ||
| 'Компания: Чуждестранна фирма', | ||
| 'ЕИК: Няма', | ||
| `Общо спечелено: ${money(0)}`, | ||
| 'Брой договори: 1', | ||
| 'Връзка: https://sigma.test/companies/foreign-corp', | ||
| ].join('\n'), | ||
| ); | ||
| }); | ||
|
|
||
| it('builds an authority citation', () => { | ||
| const a = { | ||
| name: 'Община Варна', | ||
| spentEur: 1000000, | ||
| contracts: 5, | ||
| slug: 'obshtina-varna', | ||
| }; | ||
|
|
||
| const citation = buildAuthorityCitation(a, 'https://sigma.test'); | ||
| expect(citation).toBe( | ||
| [ | ||
| 'Институция: Община Варна', | ||
| `Общо похарчено: ${money(1000000)}`, | ||
| 'Брой договори: 5', | ||
| 'Връзка: https://sigma.test/authorities/obshtina-varna', | ||
| ].join('\n'), | ||
| ); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { money, count } from '@sigma/shared'; | ||
|
|
||
| export function buildContractCitation( | ||
| c: { | ||
| subject: string; | ||
| authority: { name: string }; | ||
| bidder: { displayName: string }; | ||
| value: { currentEur: number | null }; | ||
| id: string; | ||
| }, | ||
| origin: string, | ||
| ): string { | ||
| return [ | ||
| `Договор: ${c.subject}`, | ||
| `Възложител: ${c.authority.name}`, | ||
| `Изпълнител: ${c.bidder.displayName}`, | ||
| `Стойност: ${money(c.value.currentEur)}`, | ||
| `Връзка: ${origin}/contracts/${c.id}`, | ||
| ].join('\n'); | ||
| } | ||
|
|
||
| export function buildCompanyCitation( | ||
| c: { | ||
| displayName: string; | ||
| eik: string | null; | ||
| wonEur: number; | ||
| contracts: number; | ||
| slug: string; | ||
| hasEik?: boolean; | ||
| }, | ||
| origin: string, | ||
| ): string { | ||
| return [ | ||
| `Компания: ${c.displayName}`, | ||
| `ЕИК: ${c.hasEik && c.eik ? c.eik : 'Няма'}`, | ||
| `Общо спечелено: ${money(c.wonEur)}`, | ||
| `Брой договори: ${count(c.contracts)}`, | ||
| `Връзка: ${origin}/companies/${c.slug}`, | ||
| ].join('\n'); | ||
| } | ||
|
|
||
| export function buildAuthorityCitation( | ||
| a: { | ||
| name: string; | ||
| spentEur: number; | ||
| contracts: number; | ||
| slug: string; | ||
| }, | ||
| origin: string, | ||
| ): string { | ||
| return [ | ||
| `Институция: ${a.name}`, | ||
| `Общо похарчено: ${money(a.spentEur)}`, | ||
| `Брой договори: ${count(a.contracts)}`, | ||
| `Връзка: ${origin}/authorities/${a.slug}`, | ||
| ].join('\n'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.save-btnвече е<button>, а не<a>като.source-cta. Браузърите не наследяватfont-family/font-sizeза<button>по подразбиране, така че текстът на бутона може да изглежда различно от останалата типография на сайта. Добаветеfont: inherit;в този блок за визуална консистентност.