added initial files
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
import React, { forwardRef, memo, useContext, useEffect } from 'react';
|
||||
import styled, { ThemeContext } from 'styled-components';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import InfiniteLoader from 'react-window-infinite-loader';
|
||||
import ContentLoader from 'react-content-loader';
|
||||
import { transparentize } from 'polished';
|
||||
import { openModal } from '../../../reducers/modals/actions';
|
||||
import { CURSEFORGE } from '../../../utils/constants';
|
||||
|
||||
const ModpacksListWrapper = ({
|
||||
// Are there more items to load?
|
||||
// (This information comes from the most recent API request.)
|
||||
hasNextPage,
|
||||
|
||||
// Are we currently loading a page of items?
|
||||
// (This may be an in-flight flag in your Redux store for example.)
|
||||
isNextPageLoading,
|
||||
|
||||
// Array of items loaded so far.
|
||||
items,
|
||||
|
||||
height,
|
||||
|
||||
width,
|
||||
|
||||
setStep,
|
||||
|
||||
setVersion,
|
||||
// Callback function responsible for loading the next page of items.
|
||||
loadNextPage,
|
||||
|
||||
setModpack,
|
||||
|
||||
infiniteLoaderRef
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
// If there are more items to be loaded then add an extra row to hold a loading indicator.
|
||||
const itemCount = hasNextPage ? items.length + 1 : items.length;
|
||||
// Only load 1 page of items at a time.
|
||||
// Pass an empty callback to InfiniteLoader in case it asks us to load more than once.
|
||||
const loadMoreItems = isNextPageLoading ? () => {} : loadNextPage;
|
||||
// Every row is loaded except for our loading indicator row.
|
||||
const isItemLoaded = index => !hasNextPage || index < items.length;
|
||||
|
||||
// Render an item or a loading indicator.
|
||||
const Item = memo(({ index, style }) => {
|
||||
const modpack = items[index];
|
||||
|
||||
if (!modpack) {
|
||||
return (
|
||||
<ModpackLoader
|
||||
hasNextPage={hasNextPage}
|
||||
isNextPageLoading={isNextPageLoading}
|
||||
loadNextPage={loadNextPage}
|
||||
top={style.top + (index === 0 ? 0 : 8)}
|
||||
width={width}
|
||||
height={style.height - (index === 0 ? 0 : 8)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const primaryImage = modpack?.logo;
|
||||
return (
|
||||
<div
|
||||
// eslint-disable-next-line
|
||||
style={{
|
||||
...style,
|
||||
top: style.top + (index === 0 ? 0 : 8),
|
||||
height: style.height - (index === 0 ? 0 : 8),
|
||||
background: `url('${primaryImage?.thumbnailUrl}')`,
|
||||
position: 'absolute',
|
||||
width: width - 8,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
margin: 0,
|
||||
borderRadius: 4
|
||||
}}
|
||||
key={modpack.id}
|
||||
>
|
||||
<Modpack>
|
||||
<div>{modpack.name}</div>
|
||||
</Modpack>
|
||||
<ModpackHover>
|
||||
<div
|
||||
onClick={() => {
|
||||
setVersion({
|
||||
projectID: modpack.id,
|
||||
fileID: modpack.latestFiles[modpack.latestFiles.length - 1].id,
|
||||
source: CURSEFORGE
|
||||
});
|
||||
setModpack(modpack);
|
||||
setStep(1);
|
||||
}}
|
||||
>
|
||||
Download Latest
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
openModal('ModpackDescription', {
|
||||
modpack,
|
||||
setVersion,
|
||||
setModpack,
|
||||
setStep,
|
||||
type: 'curseforge'
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
Explore / Versions
|
||||
</div>
|
||||
</ModpackHover>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const innerElementType = forwardRef(({ style, ...rest }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
// eslint-disable-next-line react/forbid-dom-props
|
||||
style={{
|
||||
...style,
|
||||
paddingTop: 0
|
||||
}}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...rest}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<InfiniteLoader
|
||||
isItemLoaded={isItemLoaded}
|
||||
itemCount={itemCount !== 0 ? itemCount : 40}
|
||||
loadMoreItems={() => loadMoreItems()}
|
||||
>
|
||||
{({ onItemsRendered }) => (
|
||||
<List
|
||||
height={height}
|
||||
width={width}
|
||||
itemCount={itemCount !== 0 ? itemCount : 40}
|
||||
itemSize={100}
|
||||
onItemsRendered={onItemsRendered}
|
||||
innerElementType={innerElementType}
|
||||
ref={list => {
|
||||
// Manually bind ref to reset scroll
|
||||
// eslint-disable-next-line
|
||||
infiniteLoaderRef.current = list;
|
||||
}}
|
||||
>
|
||||
{Item}
|
||||
</List>
|
||||
)}
|
||||
</InfiniteLoader>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ModpacksListWrapper);
|
||||
|
||||
const Modpack = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 20px;
|
||||
padding: 0 10px;
|
||||
font-weight: 700;
|
||||
background: ${props => transparentize(0.2, props.theme.palette.grey[700])};
|
||||
`;
|
||||
|
||||
const ModpackHover = styled.div`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: ${props => transparentize(0.4, props.theme.palette.grey[900])};
|
||||
opacity: 0;
|
||||
padding-left: 40%;
|
||||
will-change: opacity;
|
||||
transition: opacity 0.1s ease-in-out, background 0.1s ease-in-out;
|
||||
div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.1s ease-in-out;
|
||||
&:hover {
|
||||
background-color: ${props => props.theme.palette.primary.main};
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const ModpackLoader = memo(
|
||||
({ height, width, top, isNextPageLoading, hasNextPage, loadNextPage }) => {
|
||||
const ContextTheme = useContext(ThemeContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasNextPage && isNextPageLoading) {
|
||||
loadNextPage();
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<ContentLoader
|
||||
speed={2}
|
||||
foregroundColor={ContextTheme.palette.grey[900]}
|
||||
backgroundColor={ContextTheme.palette.grey[800]}
|
||||
title={false}
|
||||
height={height}
|
||||
style={{
|
||||
width: width - 8,
|
||||
height,
|
||||
position: 'absolute',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
top,
|
||||
borderRadius: 4
|
||||
}}
|
||||
>
|
||||
<rect x="0" y="0" width="100%" height={height} />
|
||||
</ContentLoader>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,250 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Select, Input } from 'antd';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faBomb, faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { getSearch } from '../../../api';
|
||||
import ModpacksListWrapper from './ModpacksListWrapper';
|
||||
|
||||
let lastRequest;
|
||||
const CurseForgeModpacks = ({ setStep, setVersion, setModpack }) => {
|
||||
const mcVersions = useSelector(state => state.app.vanillaManifest?.versions);
|
||||
const categories = useSelector(state => state.app.curseforgeCategories);
|
||||
const infiniteLoaderRef = useRef(null);
|
||||
const [modpacks, setModpacks] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [minecraftVersion, setMinecraftVersion] = useState(null);
|
||||
const [categoryId, setCategoryId] = useState(null);
|
||||
const [sortBy, setSortBy] = useState('Featured');
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [hasNextPage, setHasNextPage] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const updateModpacks = useDebouncedCallback(() => {
|
||||
if (infiniteLoaderRef?.current?.scrollToItem) {
|
||||
infiniteLoaderRef.current.scrollToItem(0);
|
||||
}
|
||||
loadMoreModpacks(true);
|
||||
}, 250);
|
||||
|
||||
const loadMoreModpacks = async (reset = false) => {
|
||||
const reqObj = {};
|
||||
lastRequest = reqObj;
|
||||
if (!loading) {
|
||||
setLoading(true);
|
||||
}
|
||||
if (reset && (modpacks.length !== 0 || hasNextPage)) {
|
||||
setModpacks([]);
|
||||
setHasNextPage(false);
|
||||
}
|
||||
let data = null;
|
||||
try {
|
||||
if (error) {
|
||||
setError(false);
|
||||
}
|
||||
data = await getSearch(
|
||||
'modpacks',
|
||||
searchText,
|
||||
40,
|
||||
reset ? 0 : modpacks.length,
|
||||
sortBy,
|
||||
true,
|
||||
minecraftVersion,
|
||||
categoryId
|
||||
);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
return;
|
||||
}
|
||||
const newModpacks = reset ? data : [...modpacks, ...data];
|
||||
if (lastRequest === reqObj) {
|
||||
setLoading(false);
|
||||
setHasNextPage(newModpacks.length % 40 === 0 && newModpacks.length !== 0);
|
||||
setModpacks(newModpacks);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateModpacks();
|
||||
}, [searchText, sortBy, minecraftVersion, categoryId]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<HeaderContainer>
|
||||
<StyledSelect
|
||||
placeholder="Minecraft Version"
|
||||
onChange={setMinecraftVersion}
|
||||
defaultValue={null}
|
||||
virtual={false}
|
||||
>
|
||||
<Select.Option value={null}>All Versions</Select.Option>
|
||||
{(mcVersions || [])
|
||||
.filter(v => v?.type === 'release')
|
||||
.map(v => (
|
||||
<Select.Option key={v?.id} value={v?.id}>
|
||||
{v?.id}
|
||||
</Select.Option>
|
||||
))}
|
||||
</StyledSelect>
|
||||
<StyledSelect
|
||||
placeholder="Minecraft Category"
|
||||
onChange={setCategoryId}
|
||||
defaultValue={null}
|
||||
virtual={false}
|
||||
>
|
||||
<Select.Option key="allcategories" value={null}>
|
||||
All Categories
|
||||
</Select.Option>
|
||||
{(categories || [])
|
||||
.filter(v => v?.classId === 4471)
|
||||
.sort((a, b) => a?.name.localeCompare(b?.name))
|
||||
.map(v => (
|
||||
<Select.Option value={v?.id} key={v?.id}>
|
||||
<div
|
||||
css={`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<img
|
||||
src={v?.iconUrl}
|
||||
css={`
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-right: 10px;
|
||||
`}
|
||||
alt="icon"
|
||||
/>
|
||||
{v?.name}
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</StyledSelect>
|
||||
<StyledSelect
|
||||
placeholder="Sort by"
|
||||
defaultValue="Featured"
|
||||
onChange={setSortBy}
|
||||
virtual={false}
|
||||
>
|
||||
<Select.Option key="Featured" value="Featured">
|
||||
Featured
|
||||
</Select.Option>
|
||||
<Select.Option key="Popularity" value="Popularity">
|
||||
Popularity
|
||||
</Select.Option>
|
||||
<Select.Option key="LastUpdated" value="LastUpdated">
|
||||
Last Updated
|
||||
</Select.Option>
|
||||
<Select.Option key="Name" value="Name">
|
||||
Name
|
||||
</Select.Option>
|
||||
<Select.Option key="Author" value="Author">
|
||||
Author
|
||||
</Select.Option>
|
||||
<Select.Option key="TotalDownloads" value="TotalDownloads">
|
||||
Total Downloads
|
||||
</Select.Option>
|
||||
</StyledSelect>
|
||||
<StyledInput
|
||||
placeholder="Search..."
|
||||
onSearch={setSearchText}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
</HeaderContainer>
|
||||
<ModpacksContainer>
|
||||
{!error ? (
|
||||
!loading && modpacks.length === 0 ? (
|
||||
<div
|
||||
css={`
|
||||
margin-top: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 150px;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faExclamationCircle} />
|
||||
<div
|
||||
css={`
|
||||
font-size: 20px;
|
||||
margin-top: 70px;
|
||||
`}
|
||||
>
|
||||
No modpack has been found with the current filters.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<ModpacksListWrapper
|
||||
hasNextPage={hasNextPage}
|
||||
isNextPageLoading={loading}
|
||||
items={modpacks}
|
||||
loadNextPage={loadMoreModpacks}
|
||||
width={width}
|
||||
height={height}
|
||||
setStep={setStep}
|
||||
setVersion={setVersion}
|
||||
setModpack={setModpack}
|
||||
infiniteLoaderRef={infiniteLoaderRef}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
)
|
||||
) : (
|
||||
<div
|
||||
css={`
|
||||
margin-top: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 150px;
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBomb} />
|
||||
<div
|
||||
css={`
|
||||
font-size: 20px;
|
||||
margin-top: 70px;
|
||||
`}
|
||||
>
|
||||
An error occurred while loading the modpacks list...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ModpacksContainer>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CurseForgeModpacks);
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledSelect = styled(Select)`
|
||||
width: 170px;
|
||||
margin-right: 20px;
|
||||
`;
|
||||
|
||||
const StyledInput = styled(Input.Search)``;
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const ModpacksContainer = styled.div`
|
||||
height: calc(100% - 15px);
|
||||
overflow: hidden;
|
||||
padding: 10px 0;
|
||||
`;
|
||||
Reference in New Issue
Block a user