From 2d1af335c33f64d519d96291fc5d50a411b8ff68 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Tue, 22 Aug 2023 01:29:48 -0700 Subject: [PATCH] Implement load transitions and fix resetting filters (#365) This PR implements: * Fade-in transitions when cells load in, making navigation and loading appear less janky. * When scrolling, skeleton reps show up before the actual ones load in. * Resetting filters will also reset any set inclusions or exclusions --- components/common/MentionTableField/index.tsx | 4 + components/common/MentionTypeahead/index.tsx | 18 +-- components/filters/FilterModal/index.tsx | 17 +- components/party/PartyFooter/index.tsx | 4 +- .../{ => reps}/GridRep/index.module.scss | 10 ++ components/{ => reps}/GridRep/index.tsx | 16 +- .../GridRepCollection/index.module.scss | 0 .../{ => reps}/GridRepCollection/index.tsx | 0 components/reps/LoadingRep/index.module.scss | 150 ++++++++++++++++++ components/reps/LoadingRep/index.tsx | 81 ++++++++++ pages/[username].tsx | 38 +++-- pages/saved.tsx | 39 +++-- pages/teams.tsx | 58 ++++--- styles/themes.scss | 4 + styles/variables.scss | 1 + 15 files changed, 371 insertions(+), 69 deletions(-) rename components/{ => reps}/GridRep/index.module.scss (97%) rename components/{ => reps}/GridRep/index.tsx (93%) rename components/{ => reps}/GridRepCollection/index.module.scss (100%) rename components/{ => reps}/GridRepCollection/index.tsx (100%) create mode 100644 components/reps/LoadingRep/index.module.scss create mode 100644 components/reps/LoadingRep/index.tsx diff --git a/components/common/MentionTableField/index.tsx b/components/common/MentionTableField/index.tsx index 41463780..9d023819 100644 --- a/components/common/MentionTableField/index.tsx +++ b/components/common/MentionTableField/index.tsx @@ -1,5 +1,6 @@ import TableField from '~components/common/TableField' import MentionTypeahead from '../MentionTypeahead' +import Typeahead from 'react-bootstrap-typeahead/types/core/Typeahead' interface Props extends React.DetailedHTMLProps< @@ -11,6 +12,7 @@ interface Props placeholder?: string inclusions: MentionItem[] exclusions: MentionItem[] + typeaheadRef: React.Ref onUpdate: (content: MentionItem[]) => void } @@ -20,6 +22,7 @@ const MentionTableField = ({ placeholder, inclusions, exclusions, + typeaheadRef, ...props }: Props) => { return ( @@ -31,6 +34,7 @@ const MentionTableField = ({ label={label} > { +const MentionTypeahead = React.forwardRef(function Typeahead( + { label, description, placeholder, inclusions, exclusions, ...props }: Props, + forwardedRef +) { const { t } = useTranslation('common') const locale = getCookie('NEXT_LOCALE') ? (getCookie('NEXT_LOCALE') as string) @@ -159,6 +157,7 @@ const MentionTypeahead = ({ return ( (option as MentionItem).name[locale]} defaultSelected={inclusions} filterBy={() => true} - minLength={3} onSearch={handleSearch} options={options} useCache={false} @@ -181,6 +179,6 @@ const MentionTypeahead = ({ onChange={(selected) => props.onUpdate(selected as MentionItem[])} /> ) -} +}) export default MentionTypeahead diff --git a/components/filters/FilterModal/index.tsx b/components/filters/FilterModal/index.tsx index 1ca28cd6..1bd5d2df 100644 --- a/components/filters/FilterModal/index.tsx +++ b/components/filters/FilterModal/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { getCookie, setCookie } from 'cookies-next' import { useRouter } from 'next/router' import { Trans, useTranslation } from 'react-i18next' @@ -10,16 +10,16 @@ import DialogContent from '~components/common/DialogContent' import Button from '~components/common/Button' import InputTableField from '~components/common/InputTableField' +import MentionTableField from '~components/common/MentionTableField' import SelectTableField from '~components/common/SelectTableField' import SliderTableField from '~components/common/SliderTableField' import SwitchTableField from '~components/common/SwitchTableField' import SelectItem from '~components/common/SelectItem' import type { DialogProps } from '@radix-ui/react-dialog' +import Typeahead from 'react-bootstrap-typeahead/types/core/Typeahead' import styles from './index.module.scss' -import MentionTableField from '~components/common/MentionTableField' -import classNames from 'classnames' interface Props extends DialogProps { defaultFilterSet: FilterSet @@ -43,6 +43,8 @@ const FilterModal = (props: Props) => { // Refs const headerRef = React.createRef() const footerRef = React.createRef() + const inclusionRef = useRef(null) + const exclusionRef = useRef(null) // States const [open, setOpen] = useState(false) @@ -88,7 +90,6 @@ const FilterModal = (props: Props) => { useEffect(() => { if (props.open !== undefined) { setOpen(props.open) - // When should we reset the filter state? } }) @@ -160,6 +161,12 @@ const FilterModal = (props: Props) => { setUserQuality(props.defaultFilterSet.user_quality) setNameQuality(props.defaultFilterSet.name_quality) setOriginalOnly(props.defaultFilterSet.original) + + setInclusions([]) + inclusionRef.current?.clear() + + setExclusions([]) + exclusionRef.current?.clear() } function openChange() { @@ -413,6 +420,7 @@ const FilterModal = (props: Props) => { exclusions={exclusions} placeholder={t('modals.filters.placeholders.inclusion')} label={t('modals.filters.labels.inclusion')} + typeaheadRef={inclusionRef} onUpdate={storeInclusions} /> ) @@ -424,6 +432,7 @@ const FilterModal = (props: Props) => { exclusions={inclusions} placeholder={t('modals.filters.placeholders.exclusion')} label={t('modals.filters.labels.exclusion')} + typeaheadRef={exclusionRef} onUpdate={storeExclusions} /> ) diff --git a/components/party/PartyFooter/index.tsx b/components/party/PartyFooter/index.tsx index d149de8b..9cb4830e 100644 --- a/components/party/PartyFooter/index.tsx +++ b/components/party/PartyFooter/index.tsx @@ -8,8 +8,8 @@ import DOMPurify from 'dompurify' import Button from '~components/common/Button' import SegmentedControl from '~components/common/SegmentedControl' import Segment from '~components/common/Segment' -import GridRepCollection from '~components/GridRepCollection' -import GridRep from '~components/GridRep' +import GridRepCollection from '~components/reps/GridRepCollection' +import GridRep from '~components/reps/GridRep' import RemixTeamAlert from '~components/dialogs/RemixTeamAlert' import RemixedToast from '~components/toasts/RemixedToast' import EditPartyModal from '../EditPartyModal' diff --git a/components/GridRep/index.module.scss b/components/reps/GridRep/index.module.scss similarity index 97% rename from components/GridRep/index.module.scss rename to components/reps/GridRep/index.module.scss index e208225d..b1929dd2 100644 --- a/components/GridRep/index.module.scss +++ b/components/reps/GridRep/index.module.scss @@ -9,6 +9,16 @@ padding: $unit-2x; min-width: 320px; width: 100%; + opacity: 1; + transition: opacity 0.3s ease-in-out; + + &.visible { + opacity: 1; + } + + &.hidden { + opacity: 0; + } &:hover { background: var(--grid-rep-hover); diff --git a/components/GridRep/index.tsx b/components/reps/GridRep/index.tsx similarity index 93% rename from components/GridRep/index.tsx rename to components/reps/GridRep/index.tsx index d720fe25..5be3da86 100644 --- a/components/GridRep/index.tsx +++ b/components/reps/GridRep/index.tsx @@ -40,10 +40,16 @@ const GridRep = (props: Props) => { const locale = router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' + const [visible, setVisible] = useState(false) const [mainhand, setMainhand] = useState() const [weapons, setWeapons] = useState>({}) const [grid, setGrid] = useState>({}) + const gridRepStyles = classNames({ + [styles.gridRep]: true, + [styles.visible]: visible, + [styles.hidden]: !visible, + }) const titleClass = classNames({ empty: !props.name, }) @@ -68,6 +74,14 @@ const GridRep = (props: Props) => { [styles.grid]: true, }) + useEffect(() => { + setVisible(false) // Trigger fade out + const timeout = setTimeout(() => { + setVisible(true) // Trigger fade in + }, 300) // You can adjust the timing based on your preference + return () => clearTimeout(timeout) + }, []) + useEffect(() => { const newWeapons = Array(numWeapons) const gridWeapons = Array(numWeapons) @@ -249,7 +263,7 @@ const GridRep = (props: Props) => { return ( - + {detailsWithUsername}
{generateMainhandImage()}
diff --git a/components/GridRepCollection/index.module.scss b/components/reps/GridRepCollection/index.module.scss similarity index 100% rename from components/GridRepCollection/index.module.scss rename to components/reps/GridRepCollection/index.module.scss diff --git a/components/GridRepCollection/index.tsx b/components/reps/GridRepCollection/index.tsx similarity index 100% rename from components/GridRepCollection/index.tsx rename to components/reps/GridRepCollection/index.tsx diff --git a/components/reps/LoadingRep/index.module.scss b/components/reps/LoadingRep/index.module.scss new file mode 100644 index 00000000..f5221a0d --- /dev/null +++ b/components/reps/LoadingRep/index.module.scss @@ -0,0 +1,150 @@ +.gridRep { + aspect-ratio: 3/2; + border: 1px solid transparent; + border-radius: $card-corner; + box-sizing: border-box; + display: grid; + grid-template-rows: 1fr 1fr; + gap: $unit; + padding: $unit-2x; + min-width: 320px; + width: 100%; + + .placeholder { + // background: var(--placeholder-bg); + animation-duration: 2s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: placeHolderShimmer; + animation-timing-function: linear; + background-color: #f6f7f8; + background: linear-gradient( + to right, + var(--placeholder-bg) 8%, + var(--placeholder-bg-accent) 18%, + var(--placeholder-bg) 33% + ); + background-size: 1200px 104px; + + &.small { + border-radius: calc($font-small / 2); + height: $font-small; + } + + &.regular { + border-radius: calc($font-regular / 2); + height: $font-regular; + } + } + + & > .weaponGrid { + aspect-ratio: 2/0.95; + display: grid; + grid-template-columns: 1fr 3.36fr; /* left column takes up 1 fraction, right column takes up 3 fractions */ + grid-gap: $unit; /* add a gap of 8px between grid items */ + + .weapon { + border-radius: $item-corner-small; + } + + .mainhand.weapon { + aspect-ratio: 73/153; + display: grid; + grid-column: 1 / 2; /* spans one column */ + height: calc(100% - $unit-fourth); + } + + .weapons { + display: grid; /* make the right-images container a grid */ + grid-template-columns: repeat( + 3, + 1fr + ); /* create 3 columns, each taking up 1 fraction */ + grid-template-rows: repeat( + 3, + 1fr + ); /* create 3 rows, each taking up 1 fraction */ + gap: $unit; + // column-gap: $unit; + // row-gap: $unit-2x; + } + + .grid.weapon { + aspect-ratio: 280 / 160; + display: grid; + } + + .mainhand.weapon img[src*='jpg'], + .grid.weapon img[src*='jpg'] { + border-radius: 4px; + width: 100%; + } + } + + .details { + display: flex; + flex-direction: column; + gap: $unit-half; + + .top { + display: flex; + flex-direction: row; + gap: calc($unit / 2); + align-items: center; + + .info { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: calc($unit / 2); + + .title { + width: 100%; + } + } + } + + .attributed { + display: flex; + gap: $unit-half; + align-items: center; + justify-content: space-between; + + .user { + display: flex; + flex-grow: 1; + gap: calc($unit / 2); + align-items: center; + + .image { + $diameter: 18px; + + border-radius: calc($diameter / 2); + height: $diameter; + width: $diameter; + } + + .text { + border-radius: calc($font-small / 2); + height: $font-small; + width: 40%; + } + } + + .timestamp { + width: 20%; + } + } + } +} + +@keyframes placeHolderShimmer { + $width: 400px; + + 0% { + background-position: ($width * -1) 0; + } + 100% { + background-position: $width 0; + } +} diff --git a/components/reps/LoadingRep/index.tsx b/components/reps/LoadingRep/index.tsx new file mode 100644 index 00000000..171afe3c --- /dev/null +++ b/components/reps/LoadingRep/index.tsx @@ -0,0 +1,81 @@ +import classNames from 'classnames' +import styles from './index.module.scss' + +interface Props {} + +const LoadingRep = (props: Props) => { + const numWeapons: number = 9 + + const mainhandClasses = classNames({ + [styles.weapon]: true, + [styles.mainhand]: true, + [styles.placeholder]: true, + }) + + const weaponClasses = classNames({ + [styles.weapon]: true, + [styles.grid]: true, + [styles.placeholder]: true, + }) + + return ( +
+
+
+
+
+
+ +
+
+
+
+ +
+ + +
+
+
+
+
+ +
    + {Array.from(Array(numWeapons)).map((x, i) => { + return ( +
  • + ) + })} +
+
+
+ ) +} + +export default LoadingRep diff --git a/pages/[username].tsx b/pages/[username].tsx index d462e2b6..b7a22f1b 100644 --- a/pages/[username].tsx +++ b/pages/[username].tsx @@ -15,8 +15,9 @@ import { permissiveFilterset } from '~utils/defaultFilters' import { elements, allElement } from '~data/elements' import { emptyPaginationObject } from '~utils/emptyStates' -import GridRep from '~components/GridRep' -import GridRepCollection from '~components/GridRepCollection' +import GridRep from '~components/reps/GridRep' +import GridRepCollection from '~components/reps/GridRepCollection' +import LoadingRep from '~components/reps/LoadingRep' import ErrorSection from '~components/ErrorSection' import FilterBar from '~components/filters/FilterBar' import ProfileHead from '~components/head/ProfileHead' @@ -265,6 +266,26 @@ const ProfileRoute: React.FC = ({ ) }) } + function renderLoading(number: number) { + return ( + + {Array.from(Array(number)).map((x, i) => ( + + ))} + + ) + } + + const renderInfiniteScroll = ( + 0 ? parties.length : 0} + next={() => setCurrentPage(currentPage + 1)} + hasMore={totalPages > currentPage} + loader={renderLoading(3)} + > + {renderParties()} + + ) if (context) { return ( @@ -285,18 +306,7 @@ const ProfileRoute: React.FC = ({
- 0 ? parties.length : 0} - next={() => setCurrentPage(currentPage + 1)} - hasMore={totalPages > currentPage} - loader={ -
-

{t('loading')}

-
- } - > - {renderParties()} -
+ {renderInfiniteScroll} {parties.length == 0 ? (
diff --git a/pages/saved.tsx b/pages/saved.tsx index 80d2fc4e..19ca461f 100644 --- a/pages/saved.tsx +++ b/pages/saved.tsx @@ -17,8 +17,9 @@ import { elements, allElement } from '~data/elements' import { emptyPaginationObject } from '~utils/emptyStates' import ErrorSection from '~components/ErrorSection' -import GridRep from '~components/GridRep' -import GridRepCollection from '~components/GridRepCollection' +import GridRep from '~components/reps/GridRep' +import GridRepCollection from '~components/reps/GridRepCollection' +import LoadingRep from '~components/reps/LoadingRep' import FilterBar from '~components/filters/FilterBar' import SavedHead from '~components/head/SavedHead' @@ -306,6 +307,27 @@ const SavedRoute: React.FC = ({ }) } + function renderLoading(number: number) { + return ( + + {Array.from(Array(number)).map((x, i) => ( + + ))} + + ) + } + + const renderInfiniteScroll = ( + 0 ? parties.length : 0} + next={() => setCurrentPage(currentPage + 1)} + hasMore={totalPages > currentPage} + loader={renderLoading(3)} + > + {renderParties()} + + ) + if (context) { return (
@@ -325,18 +347,7 @@ const SavedRoute: React.FC = ({
- 0 ? parties.length : 0} - next={() => setCurrentPage(currentPage + 1)} - hasMore={totalPages > currentPage} - loader={ -
-

{t('loading')}

-
- } - > - {renderParties()} -
+ {renderInfiniteScroll} {parties.length == 0 ? (
diff --git a/pages/teams.tsx b/pages/teams.tsx index 14e795b0..48904c1e 100644 --- a/pages/teams.tsx +++ b/pages/teams.tsx @@ -19,8 +19,9 @@ import { emptyPaginationObject } from '~utils/emptyStates' import { convertAdvancedFilters } from '~utils/convertAdvancedFilters' import ErrorSection from '~components/ErrorSection' -import GridRep from '~components/GridRep' -import GridRepCollection from '~components/GridRepCollection' +import GridRep from '~components/reps/GridRep' +import GridRepCollection from '~components/reps/GridRepCollection' +import LoadingRep from '~components/reps/LoadingRep' import FilterBar from '~components/filters/FilterBar' import TeamsHead from '~components/head/TeamsHead' @@ -55,6 +56,7 @@ const TeamsRoute: React.FC = ({ // Set up app-specific states const [mounted, setMounted] = useState(false) const [scrolled, setScrolled] = useState(false) + const [isLoading, setIsLoading] = useState(false) // Set up page-specific states const [parties, setParties] = useState([]) @@ -122,6 +124,8 @@ const TeamsRoute: React.FC = ({ appState.version = version } setCurrentPage(1) + + setIsLoading(false) }, []) // Add scroll event listener for shadow on FilterBar on mount @@ -151,6 +155,8 @@ const TeamsRoute: React.FC = ({ const fetchTeams = useCallback( ({ replace }: { replace: boolean }) => { + setIsLoading(true) + const filters: { [key: string]: any } = { @@ -183,6 +189,10 @@ const TeamsRoute: React.FC = ({ if (replace) replaceResults(meta.count, results) else appendResults(results) }) + .then(() => { + console.log('You are here') + setIsLoading(false) + }) .catch((error) => handleError(error)) }, [currentPage, parties, element, raid, recency, advancedFilters] @@ -318,6 +328,27 @@ const TeamsRoute: React.FC = ({ }) } + function renderLoading(number: number) { + return ( + + {Array.from(Array(number)).map((x, i) => ( + + ))} + + ) + } + + const renderInfiniteScroll = ( + 0 ? parties.length : 0} + next={() => setCurrentPage(currentPage + 1)} + hasMore={totalPages > currentPage} + loader={renderLoading(3)} + > + {renderParties()} + + ) + if (context) { return (
@@ -336,28 +367,7 @@ const TeamsRoute: React.FC = ({

{t('teams.title')}

-
- 0 ? parties.length : 0} - next={() => setCurrentPage(currentPage + 1)} - hasMore={totalPages > currentPage} - loader={ -
-

{t('loading')}

-
- } - > - {renderParties()} -
- - {parties.length == 0 ? ( -
-

{t('teams.not_found')}

-
- ) : ( - '' - )} -
+
{renderInfiniteScroll}
) } else return pageError() diff --git a/styles/themes.scss b/styles/themes.scss index 39034582..d4b18529 100644 --- a/styles/themes.scss +++ b/styles/themes.scss @@ -28,7 +28,9 @@ --selected-item-bg: #{$selected--item--bg--light}; --selected-item-bg-hover: #{$selected--item--bg--light--hover}; --anonymous-bg: #{$anonymous--bg--light}; + --placeholder-bg: #{$grey-80}; + --placeholder-bg-accent: #{$grey-75}; --transparent-stroke: #{$transparent--stroke--light}; @@ -252,7 +254,9 @@ --selected-item-bg-hover: #{$selected--item--bg--dark--hover}; --anonymous-bg: #{$anonymous--bg--dark}; + --placeholder-bg: #{$grey-40}; + --placeholder-bg-accent: #{$grey-45}; --transparent-stroke: #{$transparent--stroke--dark}; diff --git a/styles/variables.scss b/styles/variables.scss index 7e91b7d5..dc8e6336 100644 --- a/styles/variables.scss +++ b/styles/variables.scss @@ -45,6 +45,7 @@ $grey-20: #212121; $grey-25: #232323; $grey-30: #2f2f2f; $grey-40: #444; +$grey-45: #515151; $grey-50: #777; $grey-55: #888; $grey-60: #a9a9a9;