Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 37 additions & 22 deletions develop-docs/sdk/getting-started/templates/saved-replies/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,46 @@ To set up saved replies go to [GitHub > Settings > Saved replies](https://github

---

## Open an issue first (behavior/refactor -> close)
<CopyableCard title="Open an issue first (behavior/refactor -> close)" description={`Thanks for the contribution! We ask that behavioral changes and refactors have a linked issue so we can discuss the approach before a PR is opened. Could you open an issue describing the problem you're solving and your proposed approach? Closing this for now - happy to revisit once there's an issue to reference.

> Thanks for the contribution! We ask that behavioral changes and refactors have a linked issue so we can discuss the approach before a PR is opened. Could you open an issue describing the problem you're solving and your proposed approach? Closing this for now - happy to revisit once there's an issue to reference.
>
> Please also have a look at our CONTRIBUTING.md for more PR guidelines.
Please also have a look at our CONTRIBUTING.md for more PR guidelines.`}>

---
Thanks for the contribution! We ask that behavioral changes and refactors have a linked issue so we can discuss the approach before a PR is opened. Could you open an issue describing the problem you're solving and your proposed approach? Closing this for now - happy to revisit once there's an issue to reference.

## Let's discuss the approach first (idea -> close)
Please also have a look at our CONTRIBUTING.md for more PR guidelines.

> This is an interesting idea! We'd like to align on the approach before reviewing code - could you open an issue describing the problem and your proposed solution? That way we can agree on direction first. Closing this PR for now.
>
> Please also have a look at our CONTRIBUTING.md for more PR guidelines.
</CopyableCard>

---
<CopyableCard title="Let's discuss the approach first (idea -> close)" description={`This is an interesting idea! We'd like to align on the approach before reviewing code - could you open an issue describing the problem and your proposed solution? That way we can agree on direction first. Closing this PR for now.

Please also have a look at our CONTRIBUTING.md for more PR guidelines.`}>

This is an interesting idea! We'd like to align on the approach before reviewing code - could you open an issue describing the problem and your proposed solution? That way we can agree on direction first. Closing this PR for now.

Please also have a look at our CONTRIBUTING.md for more PR guidelines.

</CopyableCard>

<CopyableCard title="Not ready for review (request changes / mark as draft)" description={`Thanks for the contribution! This PR needs some updates before we can review:

- [ ] CI checks are passing
- [ ] PR description explains what and why
- [ ] Linked issue exists
- [ ] Tests are included

We marked it as draft for now. Please update and we'll take another look.

Please also have a look at our CONTRIBUTING.md for more PR guidelines.`}>

Thanks for the contribution! This PR needs some updates before we can review:

- [ ] CI checks are passing
- [ ] PR description explains what and why
- [ ] Linked issue exists
- [ ] Tests are included

We marked it as draft for now. Please update and we'll take another look.

Please also have a look at our CONTRIBUTING.md for more PR guidelines.

## Not ready for review (request changes / mark as draft)

> Thanks for the contribution! This PR needs some updates before we can review:
>
> - [ ] CI checks are passing
> - [ ] PR description explains what and why
> - [ ] Linked issue exists
> - [ ] Tests are included
>
> We marked it as draft for now. Please update and we'll take another look.
>
> Please also have a look at our CONTRIBUTING.md for more PR guidelines.
</CopyableCard>
149 changes: 149 additions & 0 deletions src/components/copyableCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
'use client';

import {Fragment, useEffect, useRef, useState} from 'react';
import {createPortal} from 'react-dom';
import {Clipboard} from 'react-feather';

import Chevron from 'sentry-docs/icons/Chevron';

interface CopyableCardProps {
children: React.ReactNode;
description: string;
title: string;
}

export function CopyableCard({title, description, children}: CopyableCardProps) {
const [copiedItem, setCopiedItem] = useState<'title' | 'description' | null>(null);
const [isOpen, setIsOpen] = useState(false);
const [isMounted, setIsMounted] = useState(false);
const buttonRef = useRef<HTMLDivElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);

useEffect(() => {
setIsMounted(true);

const handleClickOutside = (event: MouseEvent) => {
if (
buttonRef.current &&
!buttonRef.current.contains(event.target as Node) &&
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);

async function copyText(text: string, item: 'title' | 'description') {
try {
await navigator.clipboard.writeText(text.trim());
setCopiedItem(item);
setIsOpen(false);
setTimeout(() => setCopiedItem(null), 1500);
} catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to copy:', error);
}
}

const getDropdownPosition = () => {
if (!buttonRef.current) {
return {top: 0, left: 0};
}
const rect = buttonRef.current.getBoundingClientRect();
return {
top: rect.bottom + 8,
left: rect.right - 160,
};
};

const buttonClass =
'inline-flex items-center text-nowrap h-full text-gray-700 dark:text-[var(--foreground)] bg-transparent border-none cursor-pointer transition-colors duration-150 hover:bg-gray-50 dark:hover:bg-[var(--gray-a4)] active:bg-gray-100 dark:active:bg-[var(--gray-5)] focus:bg-gray-50 dark:focus:bg-[var(--gray-a4)] outline-none';
const dropdownItemClass =
'w-full p-2 px-3 text-left text-sm bg-transparent border-none rounded-md transition-colors hover:bg-gray-100 dark:hover:bg-[var(--gray-a4)] font-sans text-gray-900 dark:text-[var(--foreground)] cursor-pointer';

const getButtonLabel = () => {
if (copiedItem === 'title') {
return 'Reply title copied!';
}
if (copiedItem === 'description') {
return 'Reply description copied!';
}
return 'Copy';
};

return (
<div className="my-6 rounded-lg border border-gray-200 dark:border-[var(--gray-6)] overflow-hidden">
<div className="flex items-center justify-between gap-4 px-4 py-3 bg-gray-50 dark:bg-[var(--gray-2)] border-b border-gray-200 dark:border-[var(--gray-6)]">
<h3 className="m-0 text-base font-semibold text-gray-900 dark:text-[var(--foreground)]">
{title}
</h3>

{isMounted && (
<Fragment>
<div className="relative inline-block" ref={buttonRef}>
<div className="inline-flex items-center h-8 border border-gray-200 dark:border-[var(--gray-6)] rounded-full overflow-hidden bg-white dark:bg-[var(--gray-2)]">
<button
onClick={() => copyText(description, 'description')}
className={`${buttonClass} gap-1.5 px-3 text-sm font-medium`}
style={{borderRadius: '9999px 0 0 9999px'}}
>
<Clipboard size={14} />
<span>{getButtonLabel()}</span>
</button>

<div className="w-px h-full bg-gray-200 dark:bg-[var(--gray-6)]" />

<button
onClick={() => setIsOpen(!isOpen)}
className={`${buttonClass} px-2`}
style={{borderRadius: '0 9999px 9999px 0'}}
>
<Chevron
width={14}
height={14}
direction="down"
className={`transition-transform duration-200 ${isOpen ? 'rotate-180' : 'rotate-0'}`}
/>
</button>
</div>
</div>

{isOpen &&
createPortal(
<div
ref={dropdownRef}
className="fixed w-40 bg-white dark:bg-[var(--gray-2)] rounded-lg shadow-lg overflow-hidden z-[9999] border border-gray-300 dark:border-[var(--gray-6)]"
style={{...getDropdownPosition()}}
>
<div className="p-1">
<button
onClick={() => copyText(title, 'title')}
className={dropdownItemClass}
>
Reply title
</button>
<button
onClick={() => copyText(description, 'description')}
className={dropdownItemClass}
>
Reply description
</button>
</div>
</div>,
document.body
)}
</Fragment>
)}
</div>
<div className="p-4 bg-white dark:bg-[var(--gray-1)]">
<div className="prose dark:prose-invert max-w-none">{children}</div>
</div>
</div>
);
}
2 changes: 2 additions & 0 deletions src/mdxComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {CommunitySupportedPlatforms} from './components/communitySupportedPlatfo
import {ConfigKey} from './components/configKey';
import {ConfigValue} from './components/configValue';
import {ContentSeparator} from './components/contentSeparator';
import {CopyableCard} from './components/copyableCard';
import {CreateGitHubAppForm} from './components/createGitHubAppForm';
import {DefinitionList} from './components/definitionList';
import {DevDocsCardGrid} from './components/devDocsCardGrid';
Expand Down Expand Up @@ -114,6 +115,7 @@ export function mdxComponents(
OnboardingSteps,
RelayMetrics,
SandboxLink,
CopyableCard,
SignInNote,
SplitLayout,
SplitSection,
Expand Down
Loading