Fix party details

* Populate from SSR
* Hide button to edit empty anonymous grids
This commit is contained in:
Justin Edmund 2022-11-16 05:47:30 -08:00
parent 67aa814b27
commit 5a797f0d14
3 changed files with 313 additions and 278 deletions

View file

@ -156,6 +156,11 @@ const Party = (props: Props) => {
// Methods: Storing party data
const storeParty = function (party: Party) {
// Store the important party and state-keeping values
appState.party.name = party.name
appState.party.description = party.description
appState.party.raid = party.raid
appState.party.updated_at = party.updated_at
appState.party.id = party.id
appState.party.extra = party.extra
appState.party.user = party.user

View file

@ -1,317 +1,346 @@
import React, { useState } from 'react'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next'
import React, { useState } from "react"
import Head from "next/head"
import { useRouter } from "next/router"
import { useSnapshot } from "valtio"
import { useTranslation } from "next-i18next"
import Linkify from 'react-linkify'
import classNames from 'classnames'
import Linkify from "react-linkify"
import classNames from "classnames"
import * as AlertDialog from '@radix-ui/react-alert-dialog'
import CrossIcon from '~public/icons/Cross.svg'
import * as AlertDialog from "@radix-ui/react-alert-dialog"
import CrossIcon from "~public/icons/Cross.svg"
import Button from '~components/Button'
import CharLimitedFieldset from '~components/CharLimitedFieldset'
import RaidDropdown from '~components/RaidDropdown'
import TextFieldset from '~components/TextFieldset'
import Button from "~components/Button"
import CharLimitedFieldset from "~components/CharLimitedFieldset"
import RaidDropdown from "~components/RaidDropdown"
import TextFieldset from "~components/TextFieldset"
import { accountState } from '~utils/accountState'
import { appState } from '~utils/appState'
import { accountState } from "~utils/accountState"
import { appState } from "~utils/appState"
import './index.scss'
import Link from 'next/link'
import { formatTimeAgo } from '~utils/timeAgo'
import "./index.scss"
import Link from "next/link"
import { formatTimeAgo } from "~utils/timeAgo"
const emptyRaid: Raid = {
id: '',
name: {
en: '',
ja: ''
},
slug: '',
level: 0,
group: 0,
element: 0
id: "",
name: {
en: "",
ja: "",
},
slug: "",
level: 0,
group: 0,
element: 0,
}
// Props
interface Props {
editable: boolean
updateCallback: (name?: string, description?: string, raid?: Raid) => void
deleteCallback: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
editable: boolean
updateCallback: (name?: string, description?: string, raid?: Raid) => void
deleteCallback: (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => void
}
const PartyDetails = (props: Props) => {
const { party, raids } = useSnapshot(appState)
const { account } = useSnapshot(accountState)
const { party, raids } = useSnapshot(appState)
const { account } = useSnapshot(accountState)
const { t } = useTranslation('common')
const router = useRouter()
const locale = router.locale || 'en'
const { t } = useTranslation("common")
const router = useRouter()
const locale = router.locale || "en"
const nameInput = React.createRef<HTMLInputElement>()
const descriptionInput = React.createRef<HTMLTextAreaElement>()
const raidSelect = React.createRef<HTMLSelectElement>()
const nameInput = React.createRef<HTMLInputElement>()
const descriptionInput = React.createRef<HTMLTextAreaElement>()
const raidSelect = React.createRef<HTMLSelectElement>()
const readOnlyClasses = classNames({
'PartyDetails': true,
'ReadOnly': true,
'Visible': !party.detailsVisible
})
const readOnlyClasses = classNames({
PartyDetails: true,
ReadOnly: true,
Visible: !party.detailsVisible,
})
const editableClasses = classNames({
'PartyDetails': true,
'Editable': true,
'Visible': party.detailsVisible
})
const editableClasses = classNames({
PartyDetails: true,
Editable: true,
Visible: party.detailsVisible,
})
const emptyClasses = classNames({
'EmptyDetails': true,
'Visible': !party.detailsVisible
})
const emptyClasses = classNames({
EmptyDetails: true,
Visible: !party.detailsVisible,
})
const userClass = classNames({
'user': true,
'empty': !party.user
})
const userClass = classNames({
user: true,
empty: !party.user,
})
const linkClass = classNames({
'wind': party && party.element == 1,
'fire': party && party.element == 2,
'water': party && party.element == 3,
'earth': party && party.element == 4,
'dark': party && party.element == 5,
'light': party && party.element == 6
})
const linkClass = classNames({
wind: party && party.element == 1,
fire: party && party.element == 2,
water: party && party.element == 3,
earth: party && party.element == 4,
dark: party && party.element == 5,
light: party && party.element == 6,
})
const [errors, setErrors] = useState<{ [key: string]: string }>({
name: '',
description: ''
})
const [errors, setErrors] = useState<{ [key: string]: string }>({
name: "",
description: "",
})
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
event.preventDefault()
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
event.preventDefault()
const { name, value } = event.target
let newErrors = errors
const { name, value } = event.target
let newErrors = errors
setErrors(newErrors)
}
setErrors(newErrors)
}
function handleTextAreaChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
event.preventDefault()
function handleTextAreaChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
event.preventDefault()
const { name, value } = event.target
let newErrors = errors
const { name, value } = event.target
let newErrors = errors
setErrors(newErrors)
}
setErrors(newErrors)
}
function toggleDetails() {
appState.party.detailsVisible = !appState.party.detailsVisible
}
function toggleDetails() {
appState.party.detailsVisible = !appState.party.detailsVisible
}
function updateDetails(event: React.MouseEvent) {
const nameValue = nameInput.current?.value
const descriptionValue = descriptionInput.current?.value
const raid = raids.find(raid => raid.slug === raidSelect.current?.value)
function updateDetails(event: React.MouseEvent) {
const nameValue = nameInput.current?.value
const descriptionValue = descriptionInput.current?.value
const raid = raids.find((raid) => raid.slug === raidSelect.current?.value)
props.updateCallback(nameValue, descriptionValue, raid)
toggleDetails()
}
props.updateCallback(nameValue, descriptionValue, raid)
toggleDetails()
}
const userImage = () => {
if (party.user)
return (
<img
alt={party.user.picture.picture}
className={`profile ${party.user.picture.element}`}
srcSet={`/profile/${party.user.picture.picture}.png,
const userImage = () => {
if (party.user)
return (
<img
alt={party.user.picture.picture}
className={`profile ${party.user.picture.element}`}
srcSet={`/profile/${party.user.picture.picture}.png,
/profile/${party.user.picture.picture}@2x.png 2x`}
src={`/profile/${party.user.picture.picture}.png`}
/>
)
else
return (<div className="no-user" />)
}
const userBlock = () => {
return (
<div className={userClass}>
{ userImage() }
{ (party.user) ? party.user.username : t('no_user') }
</div>
)
}
const linkedUserBlock = (user: User) => {
return (
<div>
<Link href={`/${user.username}`} passHref>
<a className={linkClass}>{userBlock()}</a>
</Link>
</div>
)
}
const linkedRaidBlock = (raid: Raid) => {
return (
<div>
<Link href={`/teams?raid=${raid.slug}`} passHref>
<a className={`Raid ${linkClass}`}>
{raid.name[locale]}
</a>
</Link>
</div>
)
}
const deleteButton = () => {
if (party.editable) {
return (
<AlertDialog.Root>
<AlertDialog.Trigger className="Button destructive">
<span className='icon'>
<CrossIcon />
</span>
<span className="text">{t('buttons.delete')}</span>
</AlertDialog.Trigger>
<AlertDialog.Portal>
<AlertDialog.Overlay className="Overlay" />
<AlertDialog.Content className="Dialog">
<AlertDialog.Title className="DialogTitle">
{t('modals.delete_team.title')}
</AlertDialog.Title>
<AlertDialog.Description className="DialogDescription">
{t('modals.delete_team.description')}
</AlertDialog.Description>
<div className="actions">
<AlertDialog.Cancel className="Button modal">{t('modals.delete_team.buttons.cancel')}</AlertDialog.Cancel>
<AlertDialog.Action className="Button modal destructive" onClick={(e) => props.deleteCallback(e)}>{t('modals.delete_team.buttons.confirm')}</AlertDialog.Action>
</div>
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog.Root>
)
} else {
return ('')
}
}
const editable = (
<section className={editableClasses}>
<CharLimitedFieldset
fieldName="name"
placeholder="Name your team"
value={party.name}
limit={50}
onChange={handleInputChange}
error={errors.name}
ref={nameInput}
/>
<RaidDropdown
showAllRaidsOption={false}
currentRaid={party.raid?.slug || ''}
ref={raidSelect}
/>
<TextFieldset
fieldName="name"
placeholder={"Write your notes here\n\n\nWatch out for the 50% trigger!\nMake sure to click Fediels 1 first\nGood luck with RNG!"}
value={party.description}
onChange={handleTextAreaChange}
error={errors.description}
ref={descriptionInput}
/>
<div className="bottom">
<div className="left">
{ (router.pathname !== '/new') ? deleteButton() : '' }
</div>
<div className="right">
<Button
active={true}
onClick={toggleDetails}>
{t('buttons.cancel')}
</Button>
<Button
active={true}
icon="check"
onClick={updateDetails}>
{t('buttons.save_info')}
</Button>
</div>
</div>
</section>
)
const readOnly = (
<section className={readOnlyClasses}>
<div className="info">
<div className="left">
{ (party.name) ? <h1>{party.name}</h1> : '' }
<div className="attribution">
{ (party.user) ? linkedUserBlock(party.user) : userBlock() }
{ (party.raid) ? linkedRaidBlock(party.raid) : '' }
{ (party.created_at != undefined)
? <time
className="last-updated"
dateTime={new Date(party.created_at).toString()}>
{formatTimeAgo(new Date(party.created_at), locale)}
</time>
: '' }
</div>
</div>
<div className="right">
{ (party.editable)
? <Button active={true} icon="edit" onClick={toggleDetails}>{t('buttons.show_info')}</Button>
: <div /> }
</div>
</div>
{ (party.description) ? <p><Linkify>{party.description}</Linkify></p> : '' }
</section>
)
const emptyDetails = (
<div className={emptyClasses}>
<Button active={true} icon="edit" onClick={toggleDetails}>{t('buttons.show_info')}</Button>
</div>
)
const generateTitle = () => {
let title = ''
const username = (party.user != null) ? `@${party.user?.username}` : 'Anonymous'
if (party.name != null)
title = `${party.name} by ${username}`
else if (party.name == null && party.editable && router.route === '/new')
title = "New Team"
else
title = `Untitled team by ${username}`
return title
}
src={`/profile/${party.user.picture.picture}.png`}
/>
)
else return <div className="no-user" />
}
const userBlock = () => {
return (
<div>
<Head>
<title>{generateTitle()}</title>
<meta property="og:title" content={generateTitle()} />
<meta property="og:description" content={ (party.description) ? party.description : '' } />
<meta property="og:url" content="https://app.granblue.team" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="app.granblue.team" />
<meta name="twitter:title" content={generateTitle()} />
<meta name="twitter:description" content={ (party.description) ? party.description : '' } />
</Head>
{ (editable && (party.name || party.description || party.raid)) ? readOnly : emptyDetails}
{editable}
</div>
<div className={userClass}>
{userImage()}
{party.user ? party.user.username : t("no_user")}
</div>
)
}
const linkedUserBlock = (user: User) => {
return (
<div>
<Link href={`/${user.username}`} passHref>
<a className={linkClass}>{userBlock()}</a>
</Link>
</div>
)
}
const linkedRaidBlock = (raid: Raid) => {
return (
<div>
<Link href={`/teams?raid=${raid.slug}`} passHref>
<a className={`Raid ${linkClass}`}>{raid.name[locale]}</a>
</Link>
</div>
)
}
const deleteButton = () => {
if (party.editable) {
return (
<AlertDialog.Root>
<AlertDialog.Trigger className="Button destructive">
<span className="icon">
<CrossIcon />
</span>
<span className="text">{t("buttons.delete")}</span>
</AlertDialog.Trigger>
<AlertDialog.Portal>
<AlertDialog.Overlay className="Overlay" />
<AlertDialog.Content className="Dialog">
<AlertDialog.Title className="DialogTitle">
{t("modals.delete_team.title")}
</AlertDialog.Title>
<AlertDialog.Description className="DialogDescription">
{t("modals.delete_team.description")}
</AlertDialog.Description>
<div className="actions">
<AlertDialog.Cancel className="Button modal">
{t("modals.delete_team.buttons.cancel")}
</AlertDialog.Cancel>
<AlertDialog.Action
className="Button modal destructive"
onClick={(e) => props.deleteCallback(e)}
>
{t("modals.delete_team.buttons.confirm")}
</AlertDialog.Action>
</div>
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog.Root>
)
} else {
return ""
}
}
const editable = (
<section className={editableClasses}>
<CharLimitedFieldset
fieldName="name"
placeholder="Name your team"
value={party.name}
limit={50}
onChange={handleInputChange}
error={errors.name}
ref={nameInput}
/>
<RaidDropdown
showAllRaidsOption={false}
currentRaid={party.raid?.slug || ""}
ref={raidSelect}
/>
<TextFieldset
fieldName="name"
placeholder={
"Write your notes here\n\n\nWatch out for the 50% trigger!\nMake sure to click Fediels 1 first\nGood luck with RNG!"
}
value={party.description}
onChange={handleTextAreaChange}
error={errors.description}
ref={descriptionInput}
/>
<div className="bottom">
<div className="left">
{router.pathname !== "/new" ? deleteButton() : ""}
</div>
<div className="right">
<Button active={true} onClick={toggleDetails}>
{t("buttons.cancel")}
</Button>
<Button active={true} icon="check" onClick={updateDetails}>
{t("buttons.save_info")}
</Button>
</div>
</div>
</section>
)
const readOnly = (
<section className={readOnlyClasses}>
<div className="info">
<div className="left">
{party.name ? <h1>{party.name}</h1> : ""}
<div className="attribution">
{party.user ? linkedUserBlock(party.user) : userBlock()}
{party.raid ? linkedRaidBlock(party.raid) : ""}
{party.created_at != undefined ? (
<time
className="last-updated"
dateTime={new Date(party.created_at).toString()}
>
{formatTimeAgo(new Date(party.created_at), locale)}
</time>
) : (
""
)}
</div>
</div>
<div className="right">
{party.editable ? (
<Button active={true} icon="edit" onClick={toggleDetails}>
{t("buttons.show_info")}
</Button>
) : (
<div />
)}
</div>
</div>
{party.description ? (
<p>
<Linkify>{party.description}</Linkify>
</p>
) : (
""
)}
</section>
)
const emptyDetails = (
<div className={emptyClasses}>
{party.editable ? (
<Button active={true} icon="edit" onClick={toggleDetails}>
{t("buttons.show_info")}
</Button>
) : (
<div />
)}
</div>
)
const generateTitle = () => {
let title = ""
const username =
party.user != null ? `@${party.user?.username}` : "Anonymous"
if (party.name != null) title = `${party.name} by ${username}`
else if (party.name == null && party.editable && router.route === "/new")
title = "New Team"
else title = `Untitled team by ${username}`
return title
}
return (
<div>
<Head>
<title>{generateTitle()}</title>
<meta property="og:title" content={generateTitle()} />
<meta
property="og:description"
content={party.description ? party.description : ""}
/>
<meta property="og:url" content="https://app.granblue.team" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="app.granblue.team" />
<meta name="twitter:title" content={generateTitle()} />
<meta
name="twitter:description"
content={party.description ? party.description : ""}
/>
</Head>
{editable && (party.name || party.description || party.raid)
? readOnly
: emptyDetails}
{editable}
</div>
)
}
export default PartyDetails

1
types/Party.d.ts vendored
View file

@ -1,6 +1,7 @@
interface Party {
id: string
name: string
description: string
raid: Raid
shortcode: string
extra: boolean