Merge pull request #15 from jedmund/saving
Implement the ability to save your favorite grids
This commit is contained in:
commit
65d46f8f96
19 changed files with 408 additions and 30 deletions
|
|
@ -44,7 +44,7 @@ const BottomHeader = () => {
|
||||||
|
|
||||||
function deleteTeam(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
function deleteTeam(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
||||||
if (appState.party.editable && appState.party.id) {
|
if (appState.party.editable && appState.party.id) {
|
||||||
api.endpoints.parties.destroy(appState.party.id, headers)
|
api.endpoints.parties.destroy({ id: appState.party.id, params: headers })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Push to route
|
// Push to route
|
||||||
router.push('/')
|
router.push('/')
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,33 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.save:hover {
|
||||||
|
color: #FF4D4D;
|
||||||
|
|
||||||
|
.icon svg {
|
||||||
|
fill: #FF4D4D;
|
||||||
|
stroke: #FF4D4D;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.save.Active {
|
||||||
|
color: #FF4D4D;
|
||||||
|
|
||||||
|
.icon svg {
|
||||||
|
fill: #FF4D4D;
|
||||||
|
stroke: #FF4D4D;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: darken(#FF4D4D, 30);
|
||||||
|
|
||||||
|
.icon svg {
|
||||||
|
fill: darken(#FF4D4D, 30);
|
||||||
|
stroke: darken(#FF4D4D, 30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.modal:hover {
|
&.modal:hover {
|
||||||
background: $grey-90;
|
background: $grey-90;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import CrossIcon from '~public/icons/Cross.svg'
|
||||||
import EditIcon from '~public/icons/Edit.svg'
|
import EditIcon from '~public/icons/Edit.svg'
|
||||||
import LinkIcon from '~public/icons/Link.svg'
|
import LinkIcon from '~public/icons/Link.svg'
|
||||||
import MenuIcon from '~public/icons/Menu.svg'
|
import MenuIcon from '~public/icons/Menu.svg'
|
||||||
|
import SaveIcon from '~public/icons/Save.svg'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
|
|
@ -63,6 +64,10 @@ class Button extends React.Component<Props, State> {
|
||||||
icon = <span className='icon'>
|
icon = <span className='icon'>
|
||||||
<EditIcon />
|
<EditIcon />
|
||||||
</span>
|
</span>
|
||||||
|
} else if (this.props.icon === 'save') {
|
||||||
|
icon = <span className='icon stroke'>
|
||||||
|
<SaveIcon />
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
|
|
@ -70,12 +75,14 @@ class Button extends React.Component<Props, State> {
|
||||||
'Active': this.props.active,
|
'Active': this.props.active,
|
||||||
'btn-pressed': this.state.isPressed,
|
'btn-pressed': this.state.isPressed,
|
||||||
'btn-disabled': this.props.disabled,
|
'btn-disabled': this.props.disabled,
|
||||||
|
'save': this.props.icon === 'save',
|
||||||
'destructive': this.props.type == ButtonType.Destructive
|
'destructive': this.props.type == ButtonType.Destructive
|
||||||
})
|
})
|
||||||
|
|
||||||
return <button className={classes} disabled={this.props.disabled} onClick={this.props.click}>
|
return <button className={classes} disabled={this.props.disabled} onClick={this.props.click}>
|
||||||
{icon}
|
{icon}
|
||||||
<span className='text'>{this.props.children}</span>
|
{ (this.props.type != ButtonType.IconOnly) ?
|
||||||
|
<span className='text'>{this.props.children}</span> : '' }
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ const CharacterGrid = (props: Props) => {
|
||||||
|
|
||||||
// Methods: Fetching an object from the server
|
// Methods: Fetching an object from the server
|
||||||
async function fetchGrid(shortcode: string) {
|
async function fetchGrid(shortcode: string) {
|
||||||
return api.endpoints.parties.getOneWithObject({ id: shortcode, object: 'characters' })
|
return api.endpoints.parties.getOneWithObject({ id: shortcode, object: 'characters', params: headers })
|
||||||
.then(response => processResult(response))
|
.then(response => processResult(response))
|
||||||
.catch(error => processError(error))
|
.catch(error => processError(error))
|
||||||
}
|
}
|
||||||
|
|
@ -78,6 +78,8 @@ const CharacterGrid = (props: Props) => {
|
||||||
|
|
||||||
// Store the important party and state-keeping values
|
// Store the important party and state-keeping values
|
||||||
appState.party.id = party.id
|
appState.party.id = party.id
|
||||||
|
appState.party.user = party.user
|
||||||
|
appState.party.favorited = party.favorited
|
||||||
|
|
||||||
setFound(true)
|
setFound(true)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: white;
|
background: white;
|
||||||
cursor: pointer;
|
|
||||||
|
h2, .Grid {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.Grid .weapon {
|
.Grid .weapon {
|
||||||
box-shadow: inset 0 0 0 1px $grey-80;
|
box-shadow: inset 0 0 0 1px $grey-80;
|
||||||
|
|
@ -68,6 +71,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: $unit / 2;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
gap: $unit / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
button svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover,
|
||||||
|
button.Active {
|
||||||
|
background: $grey-90;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,35 @@
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
import { accountState } from '~utils/accountState'
|
||||||
import { formatTimeAgo } from '~utils/timeAgo'
|
import { formatTimeAgo } from '~utils/timeAgo'
|
||||||
|
|
||||||
|
import Button from '~components/Button'
|
||||||
|
import { ButtonType } from '~utils/enums'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
shortcode: string
|
shortcode: string
|
||||||
|
id: string
|
||||||
name: string
|
name: string
|
||||||
raid: Raid
|
raid: Raid
|
||||||
grid: GridWeapon[]
|
grid: GridWeapon[]
|
||||||
user?: User
|
user?: User
|
||||||
|
favorited: boolean
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
displayUser?: boolean | false
|
displayUser?: boolean | false
|
||||||
onClick: (shortcode: string) => void
|
onClick: (shortcode: string) => void
|
||||||
|
onSave: (partyId: string, favorited: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const GridRep = (props: Props) => {
|
const GridRep = (props: Props) => {
|
||||||
const numWeapons: number = 9
|
const numWeapons: number = 9
|
||||||
|
|
||||||
|
const { account } = useSnapshot(accountState)
|
||||||
|
|
||||||
const [mainhand, setMainhand] = useState<Weapon>()
|
const [mainhand, setMainhand] = useState<Weapon>()
|
||||||
const [weapons, setWeapons] = useState<GridArray<Weapon>>({})
|
const [weapons, setWeapons] = useState<GridArray<Weapon>>({})
|
||||||
|
|
||||||
|
|
@ -64,6 +74,10 @@ const GridRep = (props: Props) => {
|
||||||
<img alt={weapons[position]?.name.en} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapons[position]?.granblue_id}.jpg`} /> : ''
|
<img alt={weapons[position]?.name.en} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapons[position]?.granblue_id}.jpg`} /> : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendSaveData() {
|
||||||
|
props.onSave(props.id, props.favorited)
|
||||||
|
}
|
||||||
|
|
||||||
const userImage = () => {
|
const userImage = () => {
|
||||||
if (props.user)
|
if (props.user)
|
||||||
return (
|
return (
|
||||||
|
|
@ -80,7 +94,7 @@ const GridRep = (props: Props) => {
|
||||||
|
|
||||||
const details = (
|
const details = (
|
||||||
<div className="Details">
|
<div className="Details">
|
||||||
<h2 className={titleClass}>{ (props.name) ? props.name : 'Untitled' }</h2>
|
<h2 className={titleClass} onClick={navigate}>{ (props.name) ? props.name : 'Untitled' }</h2>
|
||||||
<div className="bottom">
|
<div className="bottom">
|
||||||
<div className={raidClass}>{ (props.raid) ? props.raid.name.en : 'No raid set' }</div>
|
<div className={raidClass}>{ (props.raid) ? props.raid.name.en : 'No raid set' }</div>
|
||||||
<time className="last-updated" dateTime={props.createdAt.toISOString()}>{formatTimeAgo(props.createdAt, 'en-us')}</time>
|
<time className="last-updated" dateTime={props.createdAt.toISOString()}>{formatTimeAgo(props.createdAt, 'en-us')}</time>
|
||||||
|
|
@ -90,8 +104,19 @@ const GridRep = (props: Props) => {
|
||||||
|
|
||||||
const detailsWithUsername = (
|
const detailsWithUsername = (
|
||||||
<div className="Details">
|
<div className="Details">
|
||||||
<h2 className={titleClass}>{ (props.name) ? props.name : 'Untitled' }</h2>
|
<div className="top">
|
||||||
<div className={raidClass}>{ (props.raid) ? props.raid.name.en : 'No raid set' }</div>
|
<div className="info">
|
||||||
|
<h2 className={titleClass} onClick={navigate}>{ (props.name) ? props.name : 'Untitled' }</h2>
|
||||||
|
<div className={raidClass}>{ (props.raid) ? props.raid.name.en : 'No raid set' }</div>
|
||||||
|
</div>
|
||||||
|
{ (!props.user || (account.user && account.user.id !== props.user.id)) ?
|
||||||
|
<Button
|
||||||
|
active={props.favorited}
|
||||||
|
icon="save"
|
||||||
|
type={ButtonType.IconOnly}
|
||||||
|
click={sendSaveData}
|
||||||
|
/> : ''}
|
||||||
|
</div>
|
||||||
<div className="bottom">
|
<div className="bottom">
|
||||||
<div className={userClass}>
|
<div className={userClass}>
|
||||||
{ userImage() }
|
{ userImage() }
|
||||||
|
|
@ -103,9 +128,9 @@ const GridRep = (props: Props) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="GridRep" onClick={navigate}>
|
<div className="GridRep">
|
||||||
{ (props.displayUser) ? detailsWithUsername : details}
|
{ (props.displayUser) ? detailsWithUsername : details}
|
||||||
<div className="Grid">
|
<div className="Grid" onClick={navigate}>
|
||||||
<div className="weapon grid_mainhand">
|
<div className="weapon grid_mainhand">
|
||||||
{generateMainhandImage()}
|
{generateMainhandImage()}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
bottom: $unit * 2;
|
bottom: $unit * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
#right {
|
#right > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ const HeaderMenu = (props: Props) => {
|
||||||
<Link href={`/${props.username}` || ''}>{props.username}</Link>
|
<Link href={`/${props.username}` || ''}>{props.username}</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="MenuItem">
|
<li className="MenuItem">
|
||||||
<Link href={`/${props.username}/saved` || ''}>Saved</Link>
|
<Link href={`/saved` || ''}>Saved</Link>
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
<div className="MenuGroup">
|
<div className="MenuGroup">
|
||||||
|
|
|
||||||
|
|
@ -117,13 +117,15 @@ const Party = (props: Props) => {
|
||||||
|
|
||||||
// Methods: Fetch party details
|
// Methods: Fetch party details
|
||||||
function fetchDetails(shortcode: string) {
|
function fetchDetails(shortcode: string) {
|
||||||
return api.endpoints.parties.getOne({ id: shortcode })
|
return api.endpoints.parties.getOne({ id: shortcode, params: headers })
|
||||||
.then(response => processResult(response))
|
.then(response => processResult(response))
|
||||||
.catch(error => processError(error))
|
.catch(error => processError(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
function processResult(response: AxiosResponse) {
|
function processResult(response: AxiosResponse) {
|
||||||
appState.party.id = response.data.party.id
|
appState.party.id = response.data.party.id
|
||||||
|
appState.party.user = response.data.party.user
|
||||||
|
appState.party.favorited = response.data.party.favorited
|
||||||
|
|
||||||
// Store the party's user-generated details
|
// Store the party's user-generated details
|
||||||
appState.party.name = response.data.party.name
|
appState.party.name = response.data.party.name
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ const SummonGrid = (props: Props) => {
|
||||||
|
|
||||||
// Methods: Fetching an object from the server
|
// Methods: Fetching an object from the server
|
||||||
async function fetchGrid(shortcode: string) {
|
async function fetchGrid(shortcode: string) {
|
||||||
return api.endpoints.parties.getOneWithObject({ id: shortcode, object: 'summons' })
|
return api.endpoints.parties.getOneWithObject({ id: shortcode, object: 'summons', params: headers })
|
||||||
.then(response => processResult(response))
|
.then(response => processResult(response))
|
||||||
.catch(error => processError(error))
|
.catch(error => processError(error))
|
||||||
}
|
}
|
||||||
|
|
@ -88,6 +88,8 @@ const SummonGrid = (props: Props) => {
|
||||||
|
|
||||||
// Store the important party and state-keeping values
|
// Store the important party and state-keeping values
|
||||||
appState.party.id = party.id
|
appState.party.id = party.id
|
||||||
|
appState.party.user = party.user
|
||||||
|
appState.party.favorited = party.favorited
|
||||||
|
|
||||||
setFound(true)
|
setFound(true)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,7 @@ const SummonUnit = (props: Props) => {
|
||||||
'2040094000', '2040100000', '2040080000', '2040098000',
|
'2040094000', '2040100000', '2040080000', '2040098000',
|
||||||
'2040090000', '2040084000', '2040003000', '2040056000'
|
'2040090000', '2040084000', '2040003000', '2040056000'
|
||||||
]
|
]
|
||||||
|
|
||||||
console.log(`${summon.granblue_id} ${summon.name.en} ${props.gridSummon.uncap_level} ${upgradedSummons.indexOf(summon.granblue_id.toString())}`)
|
|
||||||
let suffix = ''
|
let suffix = ''
|
||||||
if (upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 && props.gridSummon.uncap_level == 5)
|
if (upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 && props.gridSummon.uncap_level == 5)
|
||||||
suffix = '_02'
|
suffix = '_02'
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useRouter } from 'next/router'
|
||||||
import clonedeep from 'lodash.clonedeep'
|
import clonedeep from 'lodash.clonedeep'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
|
|
||||||
|
import api from '~utils/api'
|
||||||
import { accountState } from '~utils/accountState'
|
import { accountState } from '~utils/accountState'
|
||||||
import { appState, initialAppState } from '~utils/appState'
|
import { appState, initialAppState } from '~utils/appState'
|
||||||
|
|
||||||
|
|
@ -12,9 +13,14 @@ import Button from '~components/Button'
|
||||||
import HeaderMenu from '~components/HeaderMenu'
|
import HeaderMenu from '~components/HeaderMenu'
|
||||||
|
|
||||||
const TopHeader = () => {
|
const TopHeader = () => {
|
||||||
|
// Cookies
|
||||||
const [cookies, _, removeCookie] = useCookies(['user'])
|
const [cookies, _, removeCookie] = useCookies(['user'])
|
||||||
|
const headers = (cookies.user != null) ? {
|
||||||
|
'Authorization': `Bearer ${cookies.user.access_token}`
|
||||||
|
} : {}
|
||||||
|
|
||||||
const accountSnap = useSnapshot(accountState)
|
const { account } = useSnapshot(accountState)
|
||||||
|
const { party } = useSnapshot(appState)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
function copyToClipboard() {
|
function copyToClipboard() {
|
||||||
|
|
@ -53,21 +59,60 @@ const TopHeader = () => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleFavorite() {
|
||||||
|
if (party.favorited)
|
||||||
|
unsaveFavorite()
|
||||||
|
else
|
||||||
|
saveFavorite()
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveFavorite() {
|
||||||
|
if (party.id)
|
||||||
|
api.saveTeam({ id: party.id, params: headers })
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status == 201)
|
||||||
|
appState.party.favorited = true
|
||||||
|
})
|
||||||
|
else
|
||||||
|
console.error("Failed to save team: No party ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsaveFavorite() {
|
||||||
|
if (party.id)
|
||||||
|
api.unsaveTeam({ id: party.id, params: headers })
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status == 200)
|
||||||
|
appState.party.favorited = false
|
||||||
|
})
|
||||||
|
else
|
||||||
|
console.error("Failed to unsave team: No party ID")
|
||||||
|
}
|
||||||
|
|
||||||
const leftNav = () => {
|
const leftNav = () => {
|
||||||
return (
|
return (
|
||||||
<div className="dropdown">
|
<div className="dropdown">
|
||||||
<Button icon="menu">Menu</Button>
|
<Button icon="menu">Menu</Button>
|
||||||
{ (accountSnap.account.user) ?
|
{ (account.user) ?
|
||||||
<HeaderMenu authenticated={accountSnap.account.authorized} username={accountSnap.account.user.username} logout={logout} /> :
|
<HeaderMenu authenticated={account.authorized} username={account.user.username} logout={logout} /> :
|
||||||
<HeaderMenu authenticated={accountSnap.account.authorized} />
|
<HeaderMenu authenticated={account.authorized} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveButton = () => {
|
||||||
|
if (party.favorited)
|
||||||
|
return (<Button icon="save" active={true} click={toggleFavorite}>Saved</Button>)
|
||||||
|
else
|
||||||
|
return (<Button icon="save" click={toggleFavorite}>Save</Button>)
|
||||||
|
}
|
||||||
|
|
||||||
const rightNav = () => {
|
const rightNav = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{ (router.route === '/p/[party]' && account.user && (!party.user || party.user.id !== account.user.id)) ?
|
||||||
|
saveButton() : ''
|
||||||
|
}
|
||||||
{ (router.route === '/p/[party]') ?
|
{ (router.route === '/p/[party]') ?
|
||||||
<Button icon="link" click={copyToClipboard}>Copy link</Button> : ''
|
<Button icon="link" click={copyToClipboard}>Copy link</Button> : ''
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ const WeaponGrid = (props: Props) => {
|
||||||
|
|
||||||
// Methods: Fetching an object from the server
|
// Methods: Fetching an object from the server
|
||||||
async function fetchGrid(shortcode: string) {
|
async function fetchGrid(shortcode: string) {
|
||||||
return api.endpoints.parties.getOneWithObject({ id: shortcode, object: 'weapons' })
|
return api.endpoints.parties.getOneWithObject({ id: shortcode, object: 'weapons', params: headers })
|
||||||
.then(response => processResult(response))
|
.then(response => processResult(response))
|
||||||
.catch(error => processError(error))
|
.catch(error => processError(error))
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +85,8 @@ const WeaponGrid = (props: Props) => {
|
||||||
// Store the important party and state-keeping values
|
// Store the important party and state-keeping values
|
||||||
appState.party.id = party.id
|
appState.party.id = party.id
|
||||||
appState.party.extra = party.extra
|
appState.party.extra = party.extra
|
||||||
|
appState.party.user = party.user
|
||||||
|
appState.party.favorited = party.favorited
|
||||||
|
|
||||||
setFound(true)
|
setFound(true)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
|
||||||
161
pages/saved.tsx
Normal file
161
pages/saved.tsx
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { useCookies } from 'react-cookie'
|
||||||
|
import clonedeep from 'lodash.clonedeep'
|
||||||
|
|
||||||
|
import api from '~utils/api'
|
||||||
|
|
||||||
|
import GridRep from '~components/GridRep'
|
||||||
|
import GridRepCollection from '~components/GridRepCollection'
|
||||||
|
import FilterBar from '~components/FilterBar'
|
||||||
|
|
||||||
|
const SavedRoute: React.FC = () => {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// Cookies
|
||||||
|
const [cookies, _] = useCookies(['user'])
|
||||||
|
const headers = (cookies.user != null) ? {
|
||||||
|
'Authorization': `Bearer ${cookies.user.access_token}`
|
||||||
|
} : {}
|
||||||
|
|
||||||
|
const [found, setFound] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [scrolled, setScrolled] = useState(false)
|
||||||
|
const [parties, setParties] = useState<Party[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(`Fetching favorite teams...`)
|
||||||
|
fetchTeams()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener("scroll", handleScroll)
|
||||||
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
async function fetchTeams(element?: number, raid?: string, recency?: number) {
|
||||||
|
const params = {
|
||||||
|
params: {
|
||||||
|
element: (element && element >= 0) ? element : undefined,
|
||||||
|
raid: (raid && raid != '0') ? raid : undefined,
|
||||||
|
recency: (recency && recency > 0) ? recency : undefined
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${cookies.user.access_token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api.savedTeams(params)
|
||||||
|
.then(response => {
|
||||||
|
const parties: Party[] = response.data
|
||||||
|
setParties(parties.map((p: any) => p.party).sort((a, b) => (a.created_at > b.created_at) ? -1 : 1))
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
setFound(true)
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error.response != null) {
|
||||||
|
if (error.response.status == 404) {
|
||||||
|
setFound(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFavorite(teamId: string, favorited: boolean) {
|
||||||
|
if (favorited)
|
||||||
|
unsaveFavorite(teamId)
|
||||||
|
else
|
||||||
|
saveFavorite(teamId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveFavorite(teamId: string) {
|
||||||
|
api.saveTeam({ id: teamId, params: headers })
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status == 201) {
|
||||||
|
const index = parties.findIndex(p => p.id === teamId)
|
||||||
|
const party = parties[index]
|
||||||
|
|
||||||
|
party.favorited = true
|
||||||
|
|
||||||
|
let clonedParties = clonedeep(parties)
|
||||||
|
clonedParties[index] = party
|
||||||
|
|
||||||
|
setParties(clonedParties)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsaveFavorite(teamId: string) {
|
||||||
|
api.unsaveTeam({ id: teamId, params: headers })
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status == 200) {
|
||||||
|
const index = parties.findIndex(p => p.id === teamId)
|
||||||
|
const party = parties[index]
|
||||||
|
|
||||||
|
party.favorited = false
|
||||||
|
|
||||||
|
let clonedParties = clonedeep(parties)
|
||||||
|
clonedParties.splice(index, 1)
|
||||||
|
|
||||||
|
setParties(clonedParties)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScroll() {
|
||||||
|
if (window.pageYOffset > 90)
|
||||||
|
setScrolled(true)
|
||||||
|
else
|
||||||
|
setScrolled(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goTo(shortcode: string) {
|
||||||
|
router.push(`/p/${shortcode}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGrids() {
|
||||||
|
return (
|
||||||
|
<GridRepCollection>
|
||||||
|
{
|
||||||
|
parties.map((party, i) => {
|
||||||
|
return <GridRep
|
||||||
|
id={party.id}
|
||||||
|
shortcode={party.shortcode}
|
||||||
|
name={party.name}
|
||||||
|
createdAt={new Date(party.created_at)}
|
||||||
|
raid={party.raid}
|
||||||
|
grid={party.weapons}
|
||||||
|
user={party.user}
|
||||||
|
favorited={party.favorited}
|
||||||
|
key={`party-${i}`}
|
||||||
|
displayUser={true}
|
||||||
|
onClick={goTo}
|
||||||
|
onSave={toggleFavorite}
|
||||||
|
/>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</GridRepCollection>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNoGrids() {
|
||||||
|
return (
|
||||||
|
<div id="NotFound">
|
||||||
|
<h2>You haven't saved any teams yet</h2>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="Teams">
|
||||||
|
<FilterBar onFilter={fetchTeams} name="Your saved teams" scrolled={scrolled} />
|
||||||
|
{ (parties.length > 0) ? renderGrids() : renderNoGrids() }
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SavedRoute
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
import { useCookies } from 'react-cookie'
|
||||||
|
import clonedeep from 'lodash.clonedeep'
|
||||||
|
|
||||||
import api from '~utils/api'
|
import api from '~utils/api'
|
||||||
|
|
||||||
|
|
@ -10,6 +12,12 @@ import FilterBar from '~components/FilterBar'
|
||||||
const TeamsRoute: React.FC = () => {
|
const TeamsRoute: React.FC = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// Cookies
|
||||||
|
const [cookies, _] = useCookies(['user'])
|
||||||
|
const headers = (cookies.user != null) ? {
|
||||||
|
'Authorization': `Bearer ${cookies.user.access_token}`
|
||||||
|
} : {}
|
||||||
|
|
||||||
const [found, setFound] = useState(false)
|
const [found, setFound] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [scrolled, setScrolled] = useState(false)
|
const [scrolled, setScrolled] = useState(false)
|
||||||
|
|
@ -31,6 +39,9 @@ const TeamsRoute: React.FC = () => {
|
||||||
element: (element && element >= 0) ? element : undefined,
|
element: (element && element >= 0) ? element : undefined,
|
||||||
raid: (raid && raid != '0') ? raid : undefined,
|
raid: (raid && raid != '0') ? raid : undefined,
|
||||||
recency: (recency && recency > 0) ? recency : undefined
|
recency: (recency && recency > 0) ? recency : undefined
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${cookies.user.access_token}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,6 +65,47 @@ const TeamsRoute: React.FC = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleFavorite(teamId: string, favorited: boolean) {
|
||||||
|
if (favorited)
|
||||||
|
unsaveFavorite(teamId)
|
||||||
|
else
|
||||||
|
saveFavorite(teamId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveFavorite(teamId: string) {
|
||||||
|
api.saveTeam({ id: teamId, params: headers })
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status == 201) {
|
||||||
|
const index = parties.findIndex(p => p.id === teamId)
|
||||||
|
const party = parties[index]
|
||||||
|
|
||||||
|
party.favorited = true
|
||||||
|
|
||||||
|
let clonedParties = clonedeep(parties)
|
||||||
|
clonedParties[index] = party
|
||||||
|
|
||||||
|
setParties(clonedParties)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsaveFavorite(teamId: string) {
|
||||||
|
api.unsaveTeam({ id: teamId, params: headers })
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status == 200) {
|
||||||
|
const index = parties.findIndex(p => p.id === teamId)
|
||||||
|
const party = parties[index]
|
||||||
|
|
||||||
|
party.favorited = false
|
||||||
|
|
||||||
|
let clonedParties = clonedeep(parties)
|
||||||
|
clonedParties[index] = party
|
||||||
|
|
||||||
|
setParties(clonedParties)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
if (window.pageYOffset > 90)
|
if (window.pageYOffset > 90)
|
||||||
setScrolled(true)
|
setScrolled(true)
|
||||||
|
|
@ -71,15 +123,18 @@ const TeamsRoute: React.FC = () => {
|
||||||
{
|
{
|
||||||
parties.map((party, i) => {
|
parties.map((party, i) => {
|
||||||
return <GridRep
|
return <GridRep
|
||||||
|
id={party.id}
|
||||||
shortcode={party.shortcode}
|
shortcode={party.shortcode}
|
||||||
name={party.name}
|
name={party.name}
|
||||||
createdAt={new Date(party.created_at)}
|
createdAt={new Date(party.created_at)}
|
||||||
raid={party.raid}
|
raid={party.raid}
|
||||||
grid={party.weapons}
|
grid={party.weapons}
|
||||||
user={party.user}
|
user={party.user}
|
||||||
|
favorited={party.favorited}
|
||||||
key={`party-${i}`}
|
key={`party-${i}`}
|
||||||
displayUser={true}
|
displayUser={true}
|
||||||
onClick={goTo}
|
onClick={goTo}
|
||||||
|
onSave={toggleFavorite}
|
||||||
/>
|
/>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +145,7 @@ const TeamsRoute: React.FC = () => {
|
||||||
function renderNoGrids() {
|
function renderNoGrids() {
|
||||||
return (
|
return (
|
||||||
<div id="NotFound">
|
<div id="NotFound">
|
||||||
<h2>No grids found</h2>
|
<h2>No teams found</h2>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
types/Party.d.ts
vendored
1
types/Party.d.ts
vendored
|
|
@ -4,6 +4,7 @@ interface Party {
|
||||||
raid: Raid
|
raid: Raid
|
||||||
shortcode: string
|
shortcode: string
|
||||||
extra: boolean
|
extra: boolean
|
||||||
|
favorited: boolean
|
||||||
characters: Array<GridCharacter>
|
characters: Array<GridCharacter>
|
||||||
weapons: Array<GridWeapon>
|
weapons: Array<GridWeapon>
|
||||||
summons: Array<GridSummon>
|
summons: Array<GridSummon>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
import axios, { Axios, AxiosRequestConfig, AxiosResponse } from "axios"
|
import axios, { Axios, AxiosRequestConfig, AxiosResponse } from "axios"
|
||||||
|
import { appState } from "./appState"
|
||||||
|
|
||||||
interface Entity {
|
interface Entity {
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type CollectionEndpoint = (params?: {}) => Promise<AxiosResponse<any>>
|
type CollectionEndpoint = (params?: {}) => Promise<AxiosResponse<any>>
|
||||||
type IdEndpoint = ({ id }: { id: string }) => Promise<AxiosResponse<any>>
|
type IdEndpoint = ({ id, params }: { id: string, params?: {} }) => Promise<AxiosResponse<any>>
|
||||||
type IdWithObjectEndpoint = ({ id, object }: { id: string, object: string }) => Promise<AxiosResponse<any>>
|
type IdWithObjectEndpoint = ({ id, object, params }: { id: string, object: string, params?: {} }) => Promise<AxiosResponse<any>>
|
||||||
type PostEndpoint = (object: {}, headers?: {}) => Promise<AxiosResponse<any>>
|
type PostEndpoint = (object: {}, headers?: {}) => Promise<AxiosResponse<any>>
|
||||||
type PutEndpoint = (id: string, object: {}, headers?: {}) => Promise<AxiosResponse<any>>
|
type PutEndpoint = (id: string, object: {}, headers?: {}) => Promise<AxiosResponse<any>>
|
||||||
type DestroyEndpoint = (id: string, headers?: {}) => Promise<AxiosResponse<any>>
|
type DestroyEndpoint = ({ id, params }: { id: string, params?: {} }) => Promise<AxiosResponse<any>>
|
||||||
|
|
||||||
interface EndpointMap {
|
interface EndpointMap {
|
||||||
getAll: CollectionEndpoint
|
getAll: CollectionEndpoint
|
||||||
|
|
@ -42,11 +43,11 @@ class Api {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getAll: (params?: {}) => axios.get(resourceUrl, params),
|
getAll: (params?: {}) => axios.get(resourceUrl, params),
|
||||||
getOne: ({ id }: { id: string }) => axios.get(`${resourceUrl}/${id}/`),
|
getOne: ({ id, params }: { id: string, params?: {} }) => axios.get(`${resourceUrl}/${id}/`, params),
|
||||||
getOneWithObject: ({ id, object }: { id: string, object: string }) => axios.get(`${resourceUrl}/${id}/${object}`),
|
getOneWithObject: ({ id, object, params }: { id: string, object: string, params?: {} }) => axios.get(`${resourceUrl}/${id}/${object}`, params),
|
||||||
create: (object: {}, headers?: {}) => axios.post(resourceUrl, object, headers),
|
create: (object: {}, headers?: {}) => axios.post(resourceUrl, object, headers),
|
||||||
update: (id: string, object: {}, headers?: {}) => axios.put(`${resourceUrl}/${id}`, object, headers),
|
update: (id: string, object: {}, headers?: {}) => axios.put(`${resourceUrl}/${id}`, object, headers),
|
||||||
destroy: (id: string, headers?: {}) => axios.delete(`${resourceUrl}/${id}`, headers)
|
destroy: ({ id, params }: { id: string, params?: {} }) => axios.delete(`${resourceUrl}/${id}`, params)
|
||||||
} as EndpointMap
|
} as EndpointMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,6 +71,23 @@ class Api {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
savedTeams(params: {}) {
|
||||||
|
const resourceUrl = `${this.url}/parties/favorites`
|
||||||
|
return axios.get(resourceUrl, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveTeam({ id, params }: { id: string, params?: {} }) {
|
||||||
|
const body = { favorite: { party_id: id } }
|
||||||
|
const resourceUrl = `${this.url}/favorites`
|
||||||
|
return axios.post(resourceUrl, body, { headers: params })
|
||||||
|
}
|
||||||
|
|
||||||
|
unsaveTeam({ id, params }: { id: string, params?: {} }) {
|
||||||
|
const body = { favorite: { party_id: id } }
|
||||||
|
const resourceUrl = `${this.url}/favorites`
|
||||||
|
return axios.delete(resourceUrl, { data: body, headers: params })
|
||||||
|
}
|
||||||
|
|
||||||
updateUncap(resource: 'character'|'weapon'|'summon', id: string, value: number) {
|
updateUncap(resource: 'character'|'weapon'|'summon', id: string, value: number) {
|
||||||
const pluralized = resource + 's'
|
const pluralized = resource + 's'
|
||||||
const resourceUrl = `${this.url}/${pluralized}/update_uncap`
|
const resourceUrl = `${this.url}/${pluralized}/update_uncap`
|
||||||
|
|
@ -89,5 +107,6 @@ api.createEntity( { name: 'characters' })
|
||||||
api.createEntity( { name: 'weapons' })
|
api.createEntity( { name: 'weapons' })
|
||||||
api.createEntity( { name: 'summons' })
|
api.createEntity( { name: 'summons' })
|
||||||
api.createEntity( { name: 'raids' })
|
api.createEntity( { name: 'raids' })
|
||||||
|
api.createEntity( { name: 'favorites' })
|
||||||
|
|
||||||
export default api
|
export default api
|
||||||
|
|
@ -11,7 +11,9 @@ interface AppState {
|
||||||
description: string | undefined,
|
description: string | undefined,
|
||||||
raid: Raid | undefined,
|
raid: Raid | undefined,
|
||||||
element: number,
|
element: number,
|
||||||
extra: boolean
|
extra: boolean,
|
||||||
|
user: User | undefined,
|
||||||
|
favorited: boolean
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
weapons: {
|
weapons: {
|
||||||
|
|
@ -40,7 +42,9 @@ export const initialAppState: AppState = {
|
||||||
description: undefined,
|
description: undefined,
|
||||||
raid: undefined,
|
raid: undefined,
|
||||||
element: 0,
|
element: 0,
|
||||||
extra: false
|
extra: false,
|
||||||
|
user: undefined,
|
||||||
|
favorited: false
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
weapons: {
|
weapons: {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export enum ButtonType {
|
export enum ButtonType {
|
||||||
Base,
|
Base,
|
||||||
|
IconOnly,
|
||||||
Destructive
|
Destructive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue