Add toast on copy link
It animates in and out too
This commit is contained in:
parent
191f4447d5
commit
6da5a4f320
5 changed files with 122 additions and 47 deletions
|
|
@ -33,6 +33,7 @@ import Link from 'next/link'
|
||||||
import LoginModal from '~components/LoginModal'
|
import LoginModal from '~components/LoginModal'
|
||||||
import SignupModal from '~components/SignupModal'
|
import SignupModal from '~components/SignupModal'
|
||||||
import AccountModal from '~components/AccountModal'
|
import AccountModal from '~components/AccountModal'
|
||||||
|
import Toast from '~components/Toast'
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
// Localization
|
// Localization
|
||||||
|
|
@ -42,7 +43,7 @@ const Header = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
const [open, setOpen] = useState(false)
|
const [copyToastOpen, setCopyToastOpen] = useState(false)
|
||||||
const [leftMenuOpen, setLeftMenuOpen] = useState(false)
|
const [leftMenuOpen, setLeftMenuOpen] = useState(false)
|
||||||
const [rightMenuOpen, setRightMenuOpen] = useState(false)
|
const [rightMenuOpen, setRightMenuOpen] = useState(false)
|
||||||
|
|
||||||
|
|
@ -50,6 +51,14 @@ const Header = () => {
|
||||||
const { account } = useSnapshot(accountState)
|
const { account } = useSnapshot(accountState)
|
||||||
const { party } = useSnapshot(appState)
|
const { party } = useSnapshot(appState)
|
||||||
|
|
||||||
|
function handleCopyToastOpenChanged(open: boolean) {
|
||||||
|
setCopyToastOpen(open)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCopyToastCloseClicked() {
|
||||||
|
setCopyToastOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
function handleLeftMenuButtonClicked() {
|
function handleLeftMenuButtonClicked() {
|
||||||
setLeftMenuOpen(!leftMenuOpen)
|
setLeftMenuOpen(!leftMenuOpen)
|
||||||
}
|
}
|
||||||
|
|
@ -58,10 +67,6 @@ const Header = () => {
|
||||||
setRightMenuOpen(!rightMenuOpen)
|
setRightMenuOpen(!rightMenuOpen)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClickOutsideMenu() {
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLeftMenuOpenChange(open: boolean) {
|
function handleLeftMenuOpenChange(open: boolean) {
|
||||||
setLeftMenuOpen(open)
|
setLeftMenuOpen(open)
|
||||||
}
|
}
|
||||||
|
|
@ -86,11 +91,15 @@ const Header = () => {
|
||||||
el.select()
|
el.select()
|
||||||
document.execCommand('copy')
|
document.execCommand('copy')
|
||||||
el.remove()
|
el.remove()
|
||||||
|
|
||||||
|
setCopyToastOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function newParty() {
|
function handleNewParty(event: React.MouseEvent, path: string) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
// Push the root URL
|
// Push the root URL
|
||||||
router.push('/')
|
router.push(path)
|
||||||
|
|
||||||
// Clean state
|
// Clean state
|
||||||
const resetState = clonedeep(initialAppState)
|
const resetState = clonedeep(initialAppState)
|
||||||
|
|
@ -100,6 +109,9 @@ const Header = () => {
|
||||||
|
|
||||||
// Set party to be editable
|
// Set party to be editable
|
||||||
appState.party.editable = true
|
appState.party.editable = true
|
||||||
|
|
||||||
|
// Close right menu
|
||||||
|
closeRightMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
|
|
@ -141,7 +153,7 @@ const Header = () => {
|
||||||
else console.error('Failed to unsave team: No party ID')
|
else console.error('Failed to unsave team: No party ID')
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = () => {
|
const pageTitle = () => {
|
||||||
let title = ''
|
let title = ''
|
||||||
let hasAccessory = false
|
let hasAccessory = false
|
||||||
|
|
||||||
|
|
@ -178,22 +190,7 @@ const Header = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveButton = () => {
|
const profileImage = () => {
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
leftAccessoryIcon={<SaveIcon />}
|
|
||||||
className={classNames({
|
|
||||||
Save: true,
|
|
||||||
Saved: party.favorited,
|
|
||||||
})}
|
|
||||||
blended={true}
|
|
||||||
text={party.favorited ? 'Saved' : 'Save'}
|
|
||||||
onClick={toggleFavorite}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const image = () => {
|
|
||||||
let image
|
let image
|
||||||
|
|
||||||
const user = accountState.account.user
|
const user = accountState.account.user
|
||||||
|
|
@ -214,6 +211,34 @@ const Header = () => {
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const urlCopyToast = () => {
|
||||||
|
return (
|
||||||
|
<Toast
|
||||||
|
open={copyToastOpen}
|
||||||
|
duration={2400}
|
||||||
|
type="foreground"
|
||||||
|
content="This party's URL was copied to your clipboard"
|
||||||
|
onOpenChange={handleCopyToastOpenChanged}
|
||||||
|
onCloseClick={handleCopyToastCloseClicked}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveButton = () => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
leftAccessoryIcon={<SaveIcon />}
|
||||||
|
className={classNames({
|
||||||
|
Save: true,
|
||||||
|
Saved: party.favorited,
|
||||||
|
})}
|
||||||
|
blended={true}
|
||||||
|
text={party.favorited ? 'Saved' : 'Save'}
|
||||||
|
onClick={toggleFavorite}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const left = () => {
|
const left = () => {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
|
|
@ -235,7 +260,7 @@ const Header = () => {
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
{title()}
|
{pageTitle()}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -249,13 +274,6 @@ const Header = () => {
|
||||||
? saveButton()
|
? saveButton()
|
||||||
: ''}
|
: ''}
|
||||||
|
|
||||||
{/* <Button
|
|
||||||
leftAccessoryIcon={<AddIcon className="Add" />}
|
|
||||||
blended={true}
|
|
||||||
text={t('buttons.new')}
|
|
||||||
onClick={newParty}
|
|
||||||
/> */}
|
|
||||||
|
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
open={rightMenuOpen}
|
open={rightMenuOpen}
|
||||||
onOpenChange={handleRightMenuOpenChange}
|
onOpenChange={handleRightMenuOpenChange}
|
||||||
|
|
@ -263,7 +281,7 @@ const Header = () => {
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className={classNames({ Active: rightMenuOpen })}
|
className={classNames({ Active: rightMenuOpen })}
|
||||||
leftAccessoryIcon={image()}
|
leftAccessoryIcon={profileImage()}
|
||||||
rightAccessoryIcon={<ArrowIcon />}
|
rightAccessoryIcon={<ArrowIcon />}
|
||||||
rightAccessoryClassName="Arrow"
|
rightAccessoryClassName="Arrow"
|
||||||
onClick={handleRightMenuButtonClicked}
|
onClick={handleRightMenuButtonClicked}
|
||||||
|
|
@ -340,12 +358,15 @@ const Header = () => {
|
||||||
items = (
|
items = (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuGroup className="MenuGroup">
|
<DropdownMenuGroup className="MenuGroup">
|
||||||
<DropdownMenuItem className="MenuItem" onClick={closeRightMenu}>
|
<DropdownMenuItem className="MenuItem">
|
||||||
<Link href={`/new` || ''} passHref>
|
<Link
|
||||||
|
onClick={(e: React.MouseEvent) => handleNewParty(e, '/new')}
|
||||||
|
href="/new"
|
||||||
|
>
|
||||||
New party
|
New party
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="MenuItem" onClick={closeRightMenu}>
|
<DropdownMenuItem className="MenuItem">
|
||||||
<Link href={`/${account.user.username}` || ''} passHref>
|
<Link href={`/${account.user.username}` || ''} passHref>
|
||||||
Your profile
|
Your profile
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -377,7 +398,10 @@ const Header = () => {
|
||||||
<>
|
<>
|
||||||
<DropdownMenuGroup className="MenuGroup">
|
<DropdownMenuGroup className="MenuGroup">
|
||||||
<DropdownMenuItem className="MenuItem">
|
<DropdownMenuItem className="MenuItem">
|
||||||
<Link href={`/new` || ''} passHref>
|
<Link
|
||||||
|
onClick={(e: React.MouseEvent) => handleNewParty(e, '/new')}
|
||||||
|
href="/new"
|
||||||
|
>
|
||||||
New party
|
New party
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
@ -398,6 +422,7 @@ const Header = () => {
|
||||||
<nav id="Header">
|
<nav id="Header">
|
||||||
{left()}
|
{left()}
|
||||||
{right()}
|
{right()}
|
||||||
|
{urlCopyToast()}
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,33 @@
|
||||||
.Toast {
|
.Toast {
|
||||||
background: var(--dialog-bg);
|
background: var(--dialog-bg);
|
||||||
border-radius: $card-corner;
|
border-radius: $card-corner;
|
||||||
box-shadow: 0 1px 12px rgba(0, 0, 0, 0.18);
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12), 0 0 1px rgba(0, 0, 0, 0.1);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $unit-2x;
|
gap: $unit-2x;
|
||||||
padding: $unit-3x;
|
padding: $unit-3x;
|
||||||
|
|
||||||
|
&[data-state='open'] {
|
||||||
|
animation: slideLeft 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-state='closed'] {
|
||||||
|
animation: fadeOut 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-swipe='move'] {
|
||||||
|
transform: translateX(var(--radix-toast-swipe-move-x));
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-swipe='cancel'] {
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: transform 200ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-swipe='end'] {
|
||||||
|
animation: slideRight 100ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
.Header {
|
.Header {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -17,22 +17,24 @@ const Toast = ({
|
||||||
content,
|
content,
|
||||||
...props
|
...props
|
||||||
}: PropsWithChildren<Props>) => {
|
}: PropsWithChildren<Props>) => {
|
||||||
|
const { onCloseClick, ...toastProps } = props
|
||||||
|
|
||||||
const classes = classNames(props.className, {
|
const classes = classNames(props.className, {
|
||||||
Toast: true,
|
Toast: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastPrimitive.Root {...props} className={classes}>
|
<ToastPrimitive.Root {...toastProps} className={classes}>
|
||||||
<div className="Header">
|
|
||||||
{title && (
|
{title && (
|
||||||
|
<div className="Header">
|
||||||
<ToastPrimitive.Title asChild>
|
<ToastPrimitive.Title asChild>
|
||||||
<h3>{title}</h3>
|
<h3>{title}</h3>
|
||||||
</ToastPrimitive.Title>
|
</ToastPrimitive.Title>
|
||||||
)}
|
<ToastPrimitive.Close aria-label="Close" onClick={onCloseClick}>
|
||||||
<ToastPrimitive.Close aria-label="Close" onClick={props.onCloseClick}>
|
|
||||||
<span aria-hidden>×</span>
|
<span aria-hidden>×</span>
|
||||||
</ToastPrimitive.Close>
|
</ToastPrimitive.Close>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<ToastPrimitive.Description asChild>
|
<ToastPrimitive.Description asChild>
|
||||||
<p>{content}</p>
|
<p>{content}</p>
|
||||||
</ToastPrimitive.Description>
|
</ToastPrimitive.Description>
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ function MyApp({ Component, pageProps }: AppProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<ToastProvider>
|
<ToastProvider swipeDirection="right">
|
||||||
<Layout>
|
<Layout>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,15 @@ i.tag {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes openModalDesktop {
|
@keyframes openModalDesktop {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
@ -392,6 +401,24 @@ i.tag {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes slideLeft {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(var(--radix-toast-swipe-end-x));
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes fadeInFilter {
|
@keyframes fadeInFilter {
|
||||||
from {
|
from {
|
||||||
backdrop-filter: blur(5px) saturate(100%) brightness(80%) opacity(0);
|
backdrop-filter: blur(5px) saturate(100%) brightness(80%) opacity(0);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue