Merge pull request #19 from jedmund/loading-empty-states

Added a smooth loading transition + FilterBar on parties
This commit is contained in:
Justin Edmund 2022-03-01 01:12:55 -08:00 committed by GitHub
commit 4399948445
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 211 additions and 135 deletions

View file

@ -35,4 +35,28 @@
margin: 0;
max-width: 200px;
}
.UserInfo {
align-items: center;
display: flex;
flex-direction: row;
flex-grow: 1;
gap: $unit * 1.5;
img {
$diameter: $unit * 6;
border-radius: $diameter / 2;
height: $diameter;
width: $diameter;
&.gran {
background-color: #CEE7FE;
}
&.djeeta {
background-color: #FFE1FE;
}
}
}
}

View file

@ -6,7 +6,7 @@ import RaidDropdown from '~components/RaidDropdown'
import './index.scss'
interface Props {
name: string
children: React.ReactNode
scrolled: boolean
onFilter: (element?: number, raid?: string, recency?: number) => void
}
@ -31,7 +31,7 @@ const FilterBar = (props: Props) => {
return (
<div className={classes}>
<h1>{props.name}</h1>
{props.children}
<select onChange={selectChanged} ref={elementSelect}>
<option key={-1} value={-1}>All elements</option>
<option key={-0} value={0}>Null</option>

View file

@ -2,6 +2,14 @@
display: grid;
grid-template-columns: auto auto auto;
margin: 0 auto;
opacity: 0;
padding: 0;
width: fit-content;
transition: opacity 0.14s ease-in-out;
// width: fit-content;
max-width: 996px;
&.visible {
opacity: 1;
}
}

View file

@ -1,13 +1,22 @@
import classNames from 'classnames'
import React from 'react'
import './index.scss'
interface Props {}
interface Props {
loading: boolean
children: React.ReactNode
}
const GridRepCollection: React.FC<Props> = ({ children }) => {
const GridRepCollection = (props: Props) => {
const classes = classNames({
'GridRepCollection': true,
'visible': !props.loading
})
return (
<div className="GridRepCollection">
{children}
<div className={classes}>
{props.children}
</div>
)
}

View file

@ -98,9 +98,9 @@ const PartyDetails = (props: Props) => {
const readOnly = (
<section className={readOnlyClasses}>
<h1>{ (appSnapshot.party.name) ? appSnapshot.party.name : 'No title' }</h1>
{ (appSnapshot.party.name) ? <h1>appSnapshot.party.name</h1> : '' }
{ (appSnapshot.party.raid) ? <div className="Raid">{appSnapshot.party.raid.name.en}</div> : '' }
<p>{ (appSnapshot.party.description) ? appSnapshot.party.description : '' }</p>
{ (appSnapshot.party.description) ? <p>appSnapshot.party.description</p> : '' }
</section>
)

View file

@ -1,18 +1,24 @@
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useCookies } from 'react-cookie'
import api from '~utils/api'
import ProfileHeader from '~components/ProfileHeader'
import GridRep from '~components/GridRep'
import GridRepCollection from '~components/GridRepCollection'
import FilterBar from '~components/FilterBar'
const ProfileRoute: React.FC = () => {
const router = useRouter()
const { username } = router.query
const [cookies] = useCookies(['user'])
const [found, setFound] = useState(false)
const [loading, setLoading] = useState(true)
const [scrolled, setScrolled] = useState(false)
const [parties, setParties] = useState<Party[]>([])
const [user, setUser] = useState<User>({
id: '',
@ -20,13 +26,31 @@ const ProfileRoute: React.FC = () => {
granblueId: 0
})
// Filter states
const [element, setElement] = useState<number | null>(null)
const [raidId, setRaidId] = useState<string | null>(null)
const [recencyInSeconds, setRecencyInSeconds] = useState<number | null>(null)
useEffect(() => {
if (username)
fetchProfile(username as string)
}, [username])
async function fetchProfile(username: string) {
api.endpoints.users.getOne({ id: username })
const filterParams = {
params: {
element: element,
raid: raidId,
recency: recencyInSeconds
},
headers: {
'Authorization': `Bearer ${cookies.user?.access_token}`
}
}
setLoading(true)
api.endpoints.users.getOne({ id: username, params: filterParams })
.then(response => {
setUser({
id: response.data.user.id,
@ -52,6 +76,23 @@ const ProfileRoute: React.FC = () => {
})
}
function receiveFilters(element?: number, raid?: string, recency?: number) {
if (element != null && element >= 0)
setElement(element)
else
setElement(null)
if (raid && raid != '0')
setRaidId(raid)
else
setRaidId(null)
if (recency && recency > 0)
setRecencyInSeconds(recency)
else
setRecencyInSeconds(null)
}
function render() {
const content = (parties && parties.length > 0) ? renderGrids() : renderNoGrids()
return (
@ -62,55 +103,57 @@ const ProfileRoute: React.FC = () => {
)
}
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}
favorited={party.favorited}
key={`party-${i}`}
onClick={goTo}
/>
})
}
</GridRepCollection>
)
}
function renderNoGrids() {
return (
<div id="NotFound">
<h2>This user has no grids.</h2>
</div>
)
}
return (
<div id="Profile">
<FilterBar onFilter={receiveFilters} scrolled={scrolled}>
<div className="UserInfo">
<img
alt="Gran"
className="gran"
srcSet="/profile/gran.png,
/profile/gran@2x.png 2x"
src="/profile/gran.png" />
<h1>{user.username}</h1>
</div>
</FilterBar>
function renderNotFound() {
return (
<div id="NotFound">
<h2>That user doesn&apos;t exist.</h2>
</div>
)
}
if (!found && !loading) {
return renderNotFound()
} else if (found && !loading) {
return render()
} else {
return (<div />)
}
<section>
<GridRepCollection loading={loading}>
{
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}
favorited={party.favorited}
key={`party-${i}`}
onClick={goTo}
/>
})
}
</GridRepCollection>
{ (parties.length == 0) ?
<div id="NotFound">
<h2>{ (loading) ? 'Loading teams...' : 'No teams found' }</h2>
</div>
: '' }
</section>
</div>
)
}
export default ProfileRoute

View file

@ -18,12 +18,12 @@ const SavedRoute: React.FC = () => {
'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[]>([])
// Filter states
const [element, setElement] = useState<number | null>(null)
const [raidId, setRaidId] = useState<string | null>(null)
const [recencyInSeconds, setRecencyInSeconds] = useState<number | null>(null)
@ -34,9 +34,7 @@ const SavedRoute: React.FC = () => {
}, [])
const handleError = useCallback((error: any) => {
if (error.response != null && error.response.status == 404) {
setFound(false)
} else if (error.response != null) {
if (error.response != null) {
console.error(error)
} else {
console.error("There was an error.")
@ -55,13 +53,14 @@ const SavedRoute: React.FC = () => {
}
}
setLoading(true)
api.savedTeams(filterParams)
.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 => handleError(error))
@ -139,44 +138,41 @@ const SavedRoute: React.FC = () => {
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&apos;t saved any teams yet</h2>
</div>
)
}
return (
<div id="Teams">
<FilterBar onFilter={receiveFilters} name="Your saved teams" scrolled={scrolled} />
{ (parties.length > 0) ? renderGrids() : renderNoGrids() }
<FilterBar onFilter={receiveFilters} scrolled={scrolled}>
<h1>Your saved teams</h1>
</FilterBar>
<section>
<GridRepCollection loading={loading}>
{
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>
{ (parties.length == 0) ?
<div id="NotFound">
<h2>{ (loading) ? 'Loading saved teams...' : 'You haven&apos;t saved any teams yet' }</h2>
</div>
: '' }
</section>
</div>
)
}

View file

@ -18,12 +18,12 @@ const TeamsRoute: React.FC = () => {
'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[]>([])
// Filter states
const [element, setElement] = useState<number | null>(null)
const [raidId, setRaidId] = useState<string | null>(null)
const [recencyInSeconds, setRecencyInSeconds] = useState<number | null>(null)
@ -34,9 +34,7 @@ const TeamsRoute: React.FC = () => {
}, [])
const handleError = useCallback((error: any) => {
if (error.response != null && error.response.status == 404) {
setFound(false)
} else if (error.response != null) {
if (error.response != null) {
console.error(error)
} else {
console.error("There was an error.")
@ -55,13 +53,14 @@ const TeamsRoute: React.FC = () => {
}
}
setLoading(true)
api.endpoints.parties.getAll(filterParams)
.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 => handleError(error))
@ -139,44 +138,41 @@ const TeamsRoute: React.FC = () => {
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>No teams found</h2>
</div>
)
}
return (
<div id="Teams">
<FilterBar onFilter={receiveFilters} name="Discover Teams" scrolled={scrolled} />
{ (parties.length > 0) ? renderGrids() : renderNoGrids() }
<FilterBar onFilter={receiveFilters} scrolled={scrolled}>
<h1>Discover Teams</h1>
</FilterBar>
<section>
<GridRepCollection loading={loading}>
{
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>
{ (parties.length == 0) ?
<div id="NotFound">
<h2>{ (loading) ? 'Loading teams...' : 'No teams found' }</h2>
</div>
: '' }
</section>
</div>
)
}

View file

@ -161,11 +161,11 @@ select {
}
}
#Teams {
#Teams, #Profile {
display: flex;
height: 100%;
flex-direction: column;
gap: $unit * 4;
gap: $unit * 2;
}
#NotFound {